diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9977cc01bf8b..9108d380ce3c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -837,6 +837,7 @@ /eng/common/ @Azure/azure-sdk-eng /eng/tools/ @scbedd @mccoyp /.github/workflows/ @Azure/azure-sdk-eng +/.github/workflows/typespec-python-regenerate.yml @Azure/azure-sdk-eng @tadelesh @msyyc @iscai-msft @ChenxiJiang333 /.github/CODEOWNERS @lmazuel @xiangyan99 @kashifkhan @Azure/azure-sdk-eng /.github/copilot-instructions.md @l0lawrence @praveenkuttappan @maririos /.github/prompts/ @mccoyp @l0lawrence @benbp diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index aed7ede5a090..06aaecf79f2b 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -1,39 +1,31 @@ name: TypeSpec Python Regenerate Tests on: - # Trigger when eng/emitter-package.json is updated on main (branded emitter version bump) + # Trigger when eng/emitter-package.json is updated on main (uses default microsoft/typespec@main) push: branches: [main] paths: - "eng/emitter-package.json" - # Allow manual triggering with emitter choice + # Run daily at 22:00 UTC against microsoft/typespec@main + schedule: + - cron: "0 22 * * *" + # Allow manual triggering workflow_dispatch: inputs: - emitter: - description: "Which emitter to regenerate with" - required: true - type: choice - options: - - "branded (@azure-tools/typespec-python)" - - "unbranded (@typespec/http-client-python)" - default: "branded (@azure-tools/typespec-python)" - version: - description: "Emitter version (leave empty to use version from emitter-package.json for branded, or latest for unbranded)" - required: false typespec_ref: - description: "Git ref (branch, tag, or SHA) of typespec repo to build regeneration infrastructure from" + description: "Either 'main' (microsoft/typespec@main) or a microsoft/typespec pull request URL (e.g. https://github.com/microsoft/typespec/pull/1234). The PR's head repo + SHA will be checked out." required: false default: "main" - typespec_repo: - description: "TypeSpec repository (owner/repo) to checkout from (default: microsoft/typespec)" - required: false - default: "microsoft/typespec" permissions: contents: write pull-requests: write issues: write +# Note: with cancel-in-progress, a newer run can cancel an older one after it +# has force-pushed the branch but before it finishes updating the tracking +# issue. The newer run will redo the issue update, so the worst case is a +# brief stale issue body that is immediately refreshed. concurrency: group: ${{ github.workflow }} cancel-in-progress: true @@ -49,33 +41,51 @@ jobs: with: fetch-depth: 0 - - name: Determine emitter to use - id: emitter-info + - name: Resolve TypeSpec repo/ref + id: typespec-info + env: + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - if [ "${{ github.event_name }}" = "push" ]; then - # Push trigger: always use branded emitter at version from emitter-package.json - EMITTER_NAME="@azure-tools/typespec-python" - VERSION=$(jq -r '.dependencies["@azure-tools/typespec-python"]' eng/emitter-package.json) - echo "Push trigger: using branded emitter @ $VERSION" - elif [[ "${{ github.event.inputs.emitter }}" == branded* ]]; then - EMITTER_NAME="@azure-tools/typespec-python" - VERSION="${{ github.event.inputs.version }}" - if [ -z "$VERSION" ]; then - VERSION=$(jq -r '.dependencies["@azure-tools/typespec-python"]' eng/emitter-package.json) - echo "No version specified, using emitter-package.json version: $VERSION" - fi - else - EMITTER_NAME="@typespec/http-client-python" - VERSION="${{ github.event.inputs.version }}" - if [ -z "$VERSION" ]; then - VERSION=$(npm view @typespec/http-client-python version 2>/dev/null) - echo "No version specified, using latest npm version: $VERSION" + INPUT="${{ github.event.inputs.typespec_ref || 'main' }}" + # Default: microsoft/typespec @ main + REPO="microsoft/typespec" + REF="main" + DISPLAY_REF="main" + REF_URL="https://github.com/${REPO}/tree/main" + PR_NUMBER="" + + # Accept a microsoft/typespec PR URL and resolve it to head repo + SHA. + # Example: https://github.com/microsoft/typespec/pull/1234 + if [[ "$INPUT" =~ ^https://github\.com/([^/]+)/([^/]+)/pull/([0-9]+)/?$ ]]; then + PR_OWNER="${BASH_REMATCH[1]}" + PR_REPO_NAME="${BASH_REMATCH[2]}" + PR_NUMBER="${BASH_REMATCH[3]}" + if [ "$PR_OWNER/$PR_REPO_NAME" != "microsoft/typespec" ]; then + echo "::error::Only pull request URLs from microsoft/typespec are accepted (got ${PR_OWNER}/${PR_REPO_NAME})." + exit 1 fi + echo "Resolving PR #${PR_NUMBER} from ${PR_OWNER}/${PR_REPO_NAME}..." + PR_JSON=$(gh pr view "$PR_NUMBER" --repo "${PR_OWNER}/${PR_REPO_NAME}" \ + --json headRefOid,headRepositoryOwner,headRepository) + HEAD_SHA=$(echo "$PR_JSON" | jq -r '.headRefOid') + HEAD_OWNER=$(echo "$PR_JSON" | jq -r '.headRepositoryOwner.login') + HEAD_REPO_NAME=$(echo "$PR_JSON" | jq -r '.headRepository.name') + REPO="${HEAD_OWNER}/${HEAD_REPO_NAME}" + REF="${HEAD_SHA}" + DISPLAY_REF="PR #${PR_NUMBER} @ ${HEAD_SHA:0:7}" + REF_URL="${INPUT}" + elif [ "$INPUT" != "main" ]; then + echo "::error::typespec_ref must be 'main' or a microsoft/typespec pull request URL (got: ${INPUT})." + exit 1 fi - echo "emitter_name=$EMITTER_NAME" >> $GITHUB_OUTPUT - echo "emitter_version=$VERSION" >> $GITHUB_OUTPUT - echo "::notice::Regenerating with $EMITTER_NAME@$VERSION" + + echo "typespec_repo=$REPO" >> $GITHUB_OUTPUT + echo "typespec_ref=$REF" >> $GITHUB_OUTPUT + echo "typespec_display_ref=$DISPLAY_REF" >> $GITHUB_OUTPUT + echo "typespec_ref_url=$REF_URL" >> $GITHUB_OUTPUT + echo "typespec_pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "::notice::Regenerating from ${REPO}@${DISPLAY_REF}" - name: Checkout microsoft/typespec # SHA corresponds to actions/checkout@v6 @@ -84,8 +94,8 @@ jobs: # in regenerate.ts, which breaks unbranded package name detection uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: - repository: ${{ github.event.inputs.typespec_repo || 'microsoft/typespec' }} - ref: ${{ github.event.inputs.typespec_ref || 'main' }} + repository: ${{ steps.typespec-info.outputs.typespec_repo }} + ref: ${{ steps.typespec-info.outputs.typespec_ref }} path: _typespec fetch-depth: 0 @@ -107,14 +117,6 @@ jobs: npm install --ignore-scripts npm run build - - name: Install target emitter - working-directory: _typespec/packages/http-client-python - run: | - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" - VERSION="${{ steps.emitter-info.outputs.emitter_version }}" - echo "Installing $EMITTER@$VERSION" - npm install "${EMITTER}@${VERSION}" - - name: Prepare Python environment working-directory: _typespec/packages/http-client-python run: | @@ -124,8 +126,7 @@ jobs: - name: Regenerate tests working-directory: _typespec/packages/http-client-python run: | - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" - npm run regenerate -- --emitterName "$EMITTER" + npm run regenerate - name: Copy regenerated tests run: | @@ -139,16 +140,41 @@ jobs: - name: Clean up typespec checkout run: rm -rf "_typespec" + - name: Apply README template to generated test packages + run: | + set -euo pipefail + TARGET="eng/tools/emitter/gen" + TEMPLATE="$TARGET/template/README.md" + if [ ! -f "$TEMPLATE" ]; then + echo "::error::Template README not found at $TEMPLATE" + exit 1 + fi + # Replace every README.md under gen/ with the template, except: + # - the top-level gen/README.md + # - anything under gen/template/ (the template itself and any + # future siblings) + find "$TARGET" -type f -name README.md \ + ! -path "$TARGET/README.md" \ + ! -path "$TARGET/template/*" \ + -print -exec cp -f "$TEMPLATE" {} \; + - name: Commit and push changes id: push-changes run: | set -euo pipefail - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" - VERSION="${{ steps.emitter-info.outputs.emitter_version }}" - BRANCH="auto/typespec-python-regenerate" + PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}" + if [ -n "$PR_NUMBER" ]; then + SOURCE_LABEL="microsoft/typespec PR #${PR_NUMBER}" + BRANCH="auto/typespec-python-regenerate-${PR_NUMBER}" + else + SOURCE_LABEL="microsoft/typespec@main" + BRANCH="auto/typespec-python-regenerate" + fi git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" + # First, check whether regenerated content actually differs from what + # is already on disk (relative to the workflow's HEAD). git add eng/tools/emitter/gen/ if git diff --cached --quiet; then echo "No changes to commit" @@ -156,52 +182,161 @@ jobs: exit 0 fi - git checkout -B "$BRANCH" - git commit -m "[typespec-python] Regenerate tests with ${EMITTER}@${VERSION}" + # Branch off origin/main (NOT the current HEAD) so the auto branch + # never contains unrelated commits from the branch that triggered + # the workflow. In particular this avoids carrying changes to + # .github/workflows/*, which GITHUB_TOKEN is not allowed to push + # (missing `workflows` permission). + git fetch --no-tags --depth=1 origin main + git checkout -B "$BRANCH" origin/main + + # Re-apply just the regenerated gen/ tree on top of origin/main. + # HEAD@{1} is the workflow's original HEAD before the checkout above. + git checkout HEAD@{1} -- eng/tools/emitter/gen + git add eng/tools/emitter/gen/ + + # If origin/main already matches the regenerated output, there is + # nothing to push. + if git diff --cached --quiet; then + echo "No changes vs origin/main" + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 0 + fi + + git commit -m "[typespec-python] Regenerate tests from ${SOURCE_LABEL}" git push origin "$BRANCH" --force echo "has_changes=true" >> $GITHUB_OUTPUT echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - name: Create or update Pull Request - id: create-pr + - name: Create or update tracking issue with PR link + id: create-issue if: steps.push-changes.outputs.has_changes == 'true' env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" - VERSION="${{ steps.emitter-info.outputs.emitter_version }}" + TS_REF_URL="${{ steps.typespec-info.outputs.typespec_ref_url }}" + PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}" BRANCH="${{ steps.push-changes.outputs.branch }}" - TITLE="[typespec-python] Regenerate tests with ${EMITTER}@${VERSION}" - BODY="Automated regeneration of TypeSpec Python generated tests. + REPO="${{ github.repository }}" + SERVER="${{ github.server_url }}" + RUN_URL="${SERVER}/${REPO}/actions/runs/${{ github.run_id }}" - - Emitter: \`${EMITTER}@${VERSION}\` - - Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + # Use a stable source identifier so retriggering from the same `main` + # or the same PR reuses the existing tracking issue instead of + # creating a duplicate. + if [ -n "$PR_NUMBER" ]; then + SOURCE_LABEL="microsoft/typespec PR #${PR_NUMBER}" + else + SOURCE_LABEL="microsoft/typespec@main" + fi - This PR was auto-generated." + # Determine assignees. For manual (workflow_dispatch) triggers, + # assign to the user who triggered the run. For automatic triggers + # (push), fall back to the default maintainers. + EVENT_NAME="${{ github.event_name }}" + ACTOR="${{ github.actor }}" + if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$ACTOR" ]; then + ASSIGNEES="$ACTOR" + CC_LINE="cc @${ACTOR}" + else + ASSIGNEES="iscai-msft,msyyc" + CC_LINE="cc @iscai-msft @msyyc" + fi + + TITLE="[typespec-python] Regenerate tests from ${SOURCE_LABEL}" + + # Reuse an existing open tracking issue if one exists (matched by + # exact title). We resolve the issue number BEFORE building the PR + # compare URL so we can inject "Fixes #" into the prefilled PR + # body — this way the issue is closed automatically when the PR is + # merged. + # + # We list by label rather than `--search`, because GitHub's search + # tokenizer strips characters like `[`, `]`, `@`, `#` and `/`, so a + # search query over our title can be ambiguous. Listing by label + # plus an exact jq match is both faster and unambiguous. + EXISTING_ISSUE=$(gh issue list --state open --label typespec-python \ + --limit 100 --json number,title \ + | jq -r --arg title "$TITLE" '.[] | select(.title == $title) | .number' \ + | head -n1 || echo "") - # Check if a PR already exists for this branch - EXISTING_PR=$(gh pr list --head "$BRANCH" --json number --jq '.[0].number' || echo "") - if [ -n "$EXISTING_PR" ]; then - echo "Updating existing PR #$EXISTING_PR" - gh pr edit "$EXISTING_PR" --title "$TITLE" --body "$BODY" --add-reviewer iscai-msft,msyyc - PR_NUMBER="$EXISTING_PR" + if [ -n "$EXISTING_ISSUE" ]; then + ISSUE_NUMBER="$EXISTING_ISSUE" + echo "Reusing existing tracking issue #$ISSUE_NUMBER" else - echo "Creating new PR" - PR_NUMBER=$(gh pr create --head "$BRANCH" --base main \ - --title "$TITLE" --body "$BODY" \ - --reviewer iscai-msft,msyyc | grep -oP '\d+$') - echo "Created PR #$PR_NUMBER" + echo "Creating new tracking issue" + # `gh issue create` prints the new issue's URL to stdout; parse the + # trailing number out of it. (`--json`/`--jq` are not supported on + # `gh issue create`.) The body will be filled in below once we + # have the compare URL. + ISSUE_URL=$(gh issue create --title "$TITLE" \ + --body "Tracking issue for TypeSpec Python regeneration. Details will be filled in shortly." \ + --label "typespec-python" \ + --assignee "$ASSIGNEES") + ISSUE_NUMBER="${ISSUE_URL##*/}" + echo "Created issue #$ISSUE_NUMBER ($ISSUE_URL)" fi - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - - name: Enable auto-merge - if: steps.push-changes.outputs.has_changes == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - gh pr merge "${{ steps.create-pr.outputs.pr_number }}" --auto --squash || \ - echo "::warning::Auto-merge could not be enabled (may require branch protection rules)" + # Check whether an open PR already exists from this branch to main. + # If so, the tracking issue should just point at that PR instead of + # asking the user to create a new one. + EXISTING_PR_JSON=$(gh pr list --state open --head "$BRANCH" --base main \ + --json number,url --limit 1) + EXISTING_PR_URL=$(echo "$EXISTING_PR_JSON" | jq -r '.[0].url // empty') + EXISTING_PR_NUMBER=$(echo "$EXISTING_PR_JSON" | jq -r '.[0].number // empty') + + if [ -n "$EXISTING_PR_URL" ]; then + ISSUE_BODY="A pull request already exists for this regeneration. + + 👉 [View pull request #${EXISTING_PR_NUMBER}](${EXISTING_PR_URL}) + + The branch \`${BRANCH}\` was just updated with the latest regenerated tests; the existing PR will reflect those changes automatically. + + Details: + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) + - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) + - Latest workflow run: ${RUN_URL} + + ${CC_LINE}" + else + # Build a "compare" URL that opens the PR creation page pre-filled. + # GitHub Actions cannot create PRs directly (org policy), so the + # reviewer just needs to click the link to open the PR. + ISSUE_LINK="${SERVER}/${REPO}/issues/${ISSUE_NUMBER}" + PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') + PR_BODY_RAW="Fixes ${ISSUE_LINK} + + Source: ${TS_REF_URL} + + Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. + + - Workflow run: ${RUN_URL} + + This PR was auto-generated." + PR_BODY_ENC=$(jq -rn --arg b "$PR_BODY_RAW" '$b|@uri') + COMPARE_URL="${SERVER}/${REPO}/compare/main...${BRANCH}?quick_pull=1&title=${PR_TITLE_ENC}&body=${PR_BODY_ENC}" + + ISSUE_BODY="GitHub Actions is not permitted to create pull requests in this repository, so this issue tracks the regeneration instead. + + **Click the link below to open a pre-filled PR:** + + 👉 [Create pull request from \`${BRANCH}\`](${COMPARE_URL}) + + Details: + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) + - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) + - Latest workflow run: ${RUN_URL} + + ${CC_LINE}" + fi + + # Write the final body onto the tracking issue (whether reused or + # just created) and re-apply expected label and assignees. + gh issue edit "$ISSUE_NUMBER" --body "$ISSUE_BODY" \ + --add-label "typespec-python" \ + --add-assignee "$ASSIGNEES" + echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT notify-on-failure: name: "Notify on failure" @@ -215,14 +350,37 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: '[typespec-python] Regeneration workflow failed', - body: `The TypeSpec Python test regeneration workflow failed.\n\n` + - `- **Run:** ${runUrl}\n` + - `- **Trigger:** ${context.eventName}\n\n` + - `cc @iscai-msft @msyyc`, - labels: ['typespec-python'], - assignees: ['iscai-msft', 'msyyc'] + const title = '[typespec-python] Regeneration workflow failed'; + const body = `The TypeSpec Python test regeneration workflow failed.\n\n` + + `- **Run:** ${runUrl}\n` + + `- **Trigger:** ${context.eventName}\n\n` + + `cc @iscai-msft @msyyc`; + + // Look for an existing open issue with the same title; if found, + // add a comment instead of creating a duplicate. + const existing = await github.rest.search.issuesAndPullRequests({ + q: `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open in:title "${title}"`, }); + const match = existing.data.items.find( + (i) => i.title === title && !i.pull_request, + ); + + if (match) { + core.info(`Commenting on existing issue #${match.number}`); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: match.number, + body, + }); + } else { + core.info('Creating new failure-notification issue'); + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title, + body, + labels: ['typespec-python'], + assignees: ['iscai-msft', 'msyyc'], + }); + } diff --git a/eng/tools/emitter/README.md b/eng/tools/emitter/README.md index de1831a5f768..43af61d1a72e 100644 --- a/eng/tools/emitter/README.md +++ b/eng/tools/emitter/README.md @@ -4,7 +4,53 @@ This directory holds files related to the TypeSpec Python emitter (`@azure-tools ## Contents -- **`gen/`** — Auto-generated Python SDK test code produced by the emitter. - These files are regenerated automatically by the [typespec-python-regenerate](https://github.com/Azure/azure-sdk-for-python/blob/main/.github/workflows/typespec-python-regenerate.yml) workflow whenever the emitter version in `eng/emitter-package.json` is updated. +- **`gen/`** — Auto-generated Python SDK test code (`gen/azure/` and `gen/unbranded/`) produced by the emitter. + These files are regenerated by the [typespec-python-regenerate](../../../.github/workflows/typespec-python-regenerate.yml) workflow. > **Do not edit files in `gen/` by hand.** They will be overwritten on the next regeneration. + +## Regeneration workflow + +The [`typespec-python-regenerate`](../../../.github/workflows/typespec-python-regenerate.yml) GitHub Actions workflow regenerates the contents of `gen/`. + +### Triggers + +- **Automatic (push)** — runs when [`eng/emitter-package.json`](../../emitter-package.json) is updated on `main`. The workflow checks out `microsoft/typespec@main` and regenerates from there. +- **Manual (`workflow_dispatch`)** — runs from the Actions tab with one input: + - **`typespec_ref`** — either: + - `main` (default) — regenerate from `microsoft/typespec@main`, or + - a `microsoft/typespec` pull request URL (e.g. `https://github.com/microsoft/typespec/pull/1234`) — the workflow resolves the PR's head repo and SHA via `gh pr view` and checks out that exact commit (so unmerged PRs and fork branches work). + + Any other value is rejected. + +### What the workflow does + +> **Note:** When triggering from a `microsoft/typespec` pull request URL, make sure the PR is up to date with its `main` branch first — click **Update branch** on the PR before running this workflow. Otherwise the regenerated output will be based on a stale base and may produce a confusing diff that mixes PR changes with unrelated drift from `main`. + +1. Checks out `azure-sdk-for-python` and the resolved `microsoft/typespec` source into `_typespec/`. +2. Builds `_typespec/packages/http-client-python` and runs `npm run regenerate`. +3. Copies the regenerated output into `eng/tools/emitter/gen/azure` and `eng/tools/emitter/gen/unbranded`. +4. If anything changed, force-pushes the result to a branch: + - `auto/typespec-python-regenerate` when the source is `main` + - `auto/typespec-python-regenerate-` when the source is a PR URL +5. Creates or updates a tracking issue (GitHub Actions cannot open PRs directly in this repo): + - **Title** uses a stable source label so retriggering from the same source reuses the existing open issue instead of creating a duplicate: + - `[typespec-python] Regenerate tests from microsoft/typespec@main` + - `[typespec-python] Regenerate tests from microsoft/typespec PR #` + - **Body** behavior: + - If no open PR exists from the branch to `main`, the body contains a prefilled compare URL ("Create pull request from ``") that opens a PR with the title and body already populated. The prefilled PR body starts with `Fixes ` so the tracking issue is closed automatically when the PR is merged. + - If an open PR already exists from the branch to `main`, the body links directly to that PR and notes that the existing PR has been updated with the new commit (a force-push does not close the PR). + - **Assignees / cc**: + - Manual (`workflow_dispatch`) runs are assigned to the user who triggered the run (`github.actor`). + - Automatic (`push`) runs are assigned to the default maintainers (`@iscai-msft`, `@msyyc`). + +> **Note:** The auto branch is **force-pushed** on every run. If a reviewer has the compare link open and no PR has been created yet, the diff under that link will change when the next run pushes — they should reopen the link from the tracking issue to see the latest diff. If a PR already exists, the force-push updates the PR in place (it does not close it; review comments on lines that no longer exist become "outdated"). + + +### Failure notifications + +If the `regenerate` job fails, a separate `notify-on-failure` job opens a new issue titled `[typespec-python] Regeneration workflow failed`, labeled `typespec-python`, and always assigned to `@iscai-msft` and `@msyyc` regardless of trigger. + +### Concurrency + +The workflow uses `concurrency: { group: , cancel-in-progress: true }`, so a newer run cancels any in-flight run. diff --git a/eng/tools/emitter/gen/template/README.md b/eng/tools/emitter/gen/template/README.md new file mode 100644 index 000000000000..e168787b1af1 --- /dev/null +++ b/eng/tools/emitter/gen/template/README.md @@ -0,0 +1,3 @@ +# ⚠️ TEST CODE — DO NOT INSTALL FROM PYPI + +This is not a published Azure SDK package; it exists only for testing purposes. Do not install from PyPI.