From 1355b627e7e6c62c4c75920372d71e15400fc0cd Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 30 Apr 2026 11:25:55 +0800 Subject: [PATCH 01/18] Use issue with PR compare link instead of creating PR directly GitHub Actions is not permitted to create pull requests in this repo, so the regenerate workflow now creates/updates a tracking issue containing a pre-filled compare URL. Maintainers click the link to open the PR. --- .../workflows/typespec-python-regenerate.yml | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index aed7ede5a090..3517a294ce34 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -162,8 +162,8 @@ jobs: 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 }} @@ -172,36 +172,55 @@ jobs: EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" VERSION="${{ steps.emitter-info.outputs.emitter_version }}" BRANCH="${{ steps.push-changes.outputs.branch }}" + REPO="${{ github.repository }}" + SERVER="${{ github.server_url }}" + RUN_URL="${SERVER}/${REPO}/actions/runs/${{ github.run_id }}" + TITLE="[typespec-python] Regenerate tests with ${EMITTER}@${VERSION}" - BODY="Automated regeneration of TypeSpec Python generated tests. + + # 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. + PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') + PR_BODY_RAW="Automated regeneration of TypeSpec Python generated tests. - Emitter: \`${EMITTER}@${VERSION}\` - - Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + - 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: + - Emitter: \`${EMITTER}@${VERSION}\` + - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) + - Workflow run: ${RUN_URL} - # 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" + cc @iscai-msft @msyyc" + + # Reuse an existing open tracking issue if one exists (matched by title). + EXISTING_ISSUE=$(gh issue list --state open --search "\"$TITLE\" in:title" \ + --json number,title --jq ".[] | select(.title == \"$TITLE\") | .number" | head -n1 || echo "") + + if [ -n "$EXISTING_ISSUE" ]; then + echo "Updating existing issue #$EXISTING_ISSUE" + gh issue edit "$EXISTING_ISSUE" --body "$ISSUE_BODY" + ISSUE_NUMBER="$EXISTING_ISSUE" 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" + ISSUE_URL=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ + --label "typespec-python" \ + --assignee iscai-msft,msyyc) + echo "Created issue: $ISSUE_URL" + 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)" + echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT notify-on-failure: name: "Notify on failure" From 0c525c937ea580087db8b6804a2dabdedc3b9cb1 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 30 Apr 2026 11:39:12 +0800 Subject: [PATCH 02/18] Update .github/workflows/typespec-python-regenerate.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/typespec-python-regenerate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 3517a294ce34..1a83431766bd 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -206,7 +206,7 @@ jobs: # Reuse an existing open tracking issue if one exists (matched by title). EXISTING_ISSUE=$(gh issue list --state open --search "\"$TITLE\" in:title" \ - --json number,title --jq ".[] | select(.title == \"$TITLE\") | .number" | head -n1 || echo "") + --json number,title | jq -r --arg title "$TITLE" '.[] | select(.title == $title) | .number' | head -n1 || echo "") if [ -n "$EXISTING_ISSUE" ]; then echo "Updating existing issue #$EXISTING_ISSUE" From c7800462b978538bdc9e7fd595eb19dbbe68ae62 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 30 Apr 2026 11:40:34 +0800 Subject: [PATCH 03/18] Update .github/workflows/typespec-python-regenerate.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/typespec-python-regenerate.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 1a83431766bd..d643aba39a5a 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -214,11 +214,11 @@ jobs: ISSUE_NUMBER="$EXISTING_ISSUE" else echo "Creating new tracking issue" - ISSUE_URL=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ + ISSUE_NUMBER=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ --label "typespec-python" \ - --assignee iscai-msft,msyyc) - echo "Created issue: $ISSUE_URL" - ISSUE_NUMBER="${ISSUE_URL##*/}" + --assignee iscai-msft,msyyc \ + --json number --jq '.number') + echo "Created issue #$ISSUE_NUMBER" fi echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT From e05f117acb12d7fb051ac552a930d3ab8e494f95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 03:42:51 +0000 Subject: [PATCH 04/18] Enforce labels/assignees on existing issue update Agent-Logs-Url: https://github.com/Azure/azure-sdk-for-python/sessions/7245d6ed-22c5-48c9-8004-9edecc51602d Co-authored-by: msyyc <70930885+msyyc@users.noreply.github.com> --- .github/workflows/typespec-python-regenerate.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index d643aba39a5a..2f7f254cf662 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -210,7 +210,10 @@ jobs: if [ -n "$EXISTING_ISSUE" ]; then echo "Updating existing issue #$EXISTING_ISSUE" - gh issue edit "$EXISTING_ISSUE" --body "$ISSUE_BODY" + # Re-apply expected label and assignees in case they were removed from the existing issue. + gh issue edit "$EXISTING_ISSUE" --body "$ISSUE_BODY" \ + --add-label "typespec-python" \ + --add-assignee iscai-msft,msyyc ISSUE_NUMBER="$EXISTING_ISSUE" else echo "Creating new tracking issue" From 7927e3db0fc56d7f49eba0629e2f956e7c5bd5dc Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 30 Apr 2026 11:56:25 +0800 Subject: [PATCH 05/18] Update typespec-python-regenerate.yml --- .github/workflows/typespec-python-regenerate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 2f7f254cf662..5c1f4264ba42 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -125,7 +125,7 @@ jobs: 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: | From 37846dd8c499cef0fff9ef89a31c849ed3c96145 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 02:45:06 +0000 Subject: [PATCH 06/18] simplify input --- .../workflows/typespec-python-regenerate.yml | 76 +++++-------------- 1 file changed, 19 insertions(+), 57 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 5c1f4264ba42..53b388986153 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -1,25 +1,14 @@ 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 + # 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" required: false @@ -49,33 +38,15 @@ jobs: with: fetch-depth: 0 - - name: Determine emitter to use - id: emitter-info + - name: Resolve TypeSpec repo/ref + id: typespec-info 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" - fi - fi - echo "emitter_name=$EMITTER_NAME" >> $GITHUB_OUTPUT - echo "emitter_version=$VERSION" >> $GITHUB_OUTPUT - echo "::notice::Regenerating with $EMITTER_NAME@$VERSION" + REPO="${{ github.event.inputs.typespec_repo || 'microsoft/typespec' }}" + REF="${{ github.event.inputs.typespec_ref || 'main' }}" + echo "typespec_repo=$REPO" >> $GITHUB_OUTPUT + echo "typespec_ref=$REF" >> $GITHUB_OUTPUT + echo "::notice::Regenerating from ${REPO}@${REF}" - name: Checkout microsoft/typespec # SHA corresponds to actions/checkout@v6 @@ -84,8 +55,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 +78,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,7 +87,6 @@ jobs: - name: Regenerate tests working-directory: _typespec/packages/http-client-python run: | - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" npm run regenerate - name: Copy regenerated tests @@ -143,8 +105,7 @@ jobs: id: push-changes run: | set -euo pipefail - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" - VERSION="${{ steps.emitter-info.outputs.emitter_version }}" + REPO_REF="${{ steps.typespec-info.outputs.typespec_repo }}@${{ steps.typespec-info.outputs.typespec_ref }}" BRANCH="auto/typespec-python-regenerate" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -157,7 +118,7 @@ jobs: fi git checkout -B "$BRANCH" - git commit -m "[typespec-python] Regenerate tests with ${EMITTER}@${VERSION}" + git commit -m "[typespec-python] Regenerate tests from ${REPO_REF}" git push origin "$BRANCH" --force echo "has_changes=true" >> $GITHUB_OUTPUT echo "branch=$BRANCH" >> $GITHUB_OUTPUT @@ -169,14 +130,15 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - EMITTER="${{ steps.emitter-info.outputs.emitter_name }}" - VERSION="${{ steps.emitter-info.outputs.emitter_version }}" + TS_REPO="${{ steps.typespec-info.outputs.typespec_repo }}" + TS_REF="${{ steps.typespec-info.outputs.typespec_ref }}" BRANCH="${{ steps.push-changes.outputs.branch }}" REPO="${{ github.repository }}" SERVER="${{ github.server_url }}" RUN_URL="${SERVER}/${REPO}/actions/runs/${{ github.run_id }}" + TS_REF_URL="${SERVER}/${TS_REPO}/tree/${TS_REF}" - TITLE="[typespec-python] Regenerate tests with ${EMITTER}@${VERSION}" + TITLE="[typespec-python] Regenerate tests from ${TS_REPO}@${TS_REF}" # Build a "compare" URL that opens the PR creation page pre-filled. # GitHub Actions cannot create PRs directly (org policy), so the @@ -184,7 +146,7 @@ jobs: PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') PR_BODY_RAW="Automated regeneration of TypeSpec Python generated tests. - - Emitter: \`${EMITTER}@${VERSION}\` + - Source: \`${TS_REPO}@${TS_REF}\` (${TS_REF_URL}) - Workflow run: ${RUN_URL} This PR was auto-generated." @@ -198,7 +160,7 @@ jobs: 👉 [Create pull request from \`${BRANCH}\`](${COMPARE_URL}) Details: - - Emitter: \`${EMITTER}@${VERSION}\` + - Source: [\`${TS_REPO}@${TS_REF}\`](${TS_REF_URL}) - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) - Workflow run: ${RUN_URL} From b696ebece87d42f366bc5c697b35c8b814dd8e22 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 02:47:46 +0000 Subject: [PATCH 07/18] add codeowner for typespec-python-regenerate action --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) 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 From fa40875949ab3c195a8a62616cb4c6684cdedcb3 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 03:21:36 +0000 Subject: [PATCH 08/18] update --- .../workflows/typespec-python-regenerate.yml | 85 ++++++++++++++----- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 53b388986153..29bf96a8bedd 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -10,13 +10,9 @@ on: workflow_dispatch: inputs: 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 @@ -40,13 +36,49 @@ jobs: - name: Resolve TypeSpec repo/ref id: typespec-info + env: + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - REPO="${{ github.event.inputs.typespec_repo || 'microsoft/typespec' }}" - REF="${{ github.event.inputs.typespec_ref || 'main' }}" + 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 "typespec_repo=$REPO" >> $GITHUB_OUTPUT echo "typespec_ref=$REF" >> $GITHUB_OUTPUT - echo "::notice::Regenerating from ${REPO}@${REF}" + 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 @@ -105,8 +137,14 @@ jobs: id: push-changes run: | set -euo pipefail - REPO_REF="${{ steps.typespec-info.outputs.typespec_repo }}@${{ steps.typespec-info.outputs.typespec_ref }}" - 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" @@ -118,7 +156,7 @@ jobs: fi git checkout -B "$BRANCH" - git commit -m "[typespec-python] Regenerate tests from ${REPO_REF}" + 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 @@ -130,23 +168,32 @@ jobs: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - TS_REPO="${{ steps.typespec-info.outputs.typespec_repo }}" - TS_REF="${{ steps.typespec-info.outputs.typespec_ref }}" + 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 }}" REPO="${{ github.repository }}" SERVER="${{ github.server_url }}" RUN_URL="${SERVER}/${REPO}/actions/runs/${{ github.run_id }}" - TS_REF_URL="${SERVER}/${TS_REPO}/tree/${TS_REF}" - TITLE="[typespec-python] Regenerate tests from ${TS_REPO}@${TS_REF}" + # 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 + + TITLE="[typespec-python] Regenerate tests from ${SOURCE_LABEL}" # 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. PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') - PR_BODY_RAW="Automated regeneration of TypeSpec Python generated tests. + PR_BODY_RAW="Source: ${TS_REF_URL} + + Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. - - Source: \`${TS_REPO}@${TS_REF}\` (${TS_REF_URL}) - Workflow run: ${RUN_URL} This PR was auto-generated." @@ -160,9 +207,9 @@ jobs: 👉 [Create pull request from \`${BRANCH}\`](${COMPARE_URL}) Details: - - Source: [\`${TS_REPO}@${TS_REF}\`](${TS_REF_URL}) + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) - - Workflow run: ${RUN_URL} + - Latest workflow run: ${RUN_URL} cc @iscai-msft @msyyc" From c9e9befd874caa9a79773a353dc58675e8c2f5e9 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 03:26:07 +0000 Subject: [PATCH 09/18] update issue title and body --- .../workflows/typespec-python-regenerate.yml | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 29bf96a8bedd..59d769509d5a 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -186,32 +186,55 @@ jobs: TITLE="[typespec-python] Regenerate tests from ${SOURCE_LABEL}" - # 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. - PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') - PR_BODY_RAW="Source: ${TS_REF_URL} + # 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') - Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. + if [ -n "$EXISTING_PR_URL" ]; then + ISSUE_BODY="A pull request already exists for this regeneration. - - Workflow run: ${RUN_URL} + 👉 [View pull request #${EXISTING_PR_NUMBER}](${EXISTING_PR_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}" + The branch \`${BRANCH}\` was just updated with the latest regenerated tests; the existing PR will reflect those changes automatically. - ISSUE_BODY="GitHub Actions is not permitted to create pull requests in this repository, so this issue tracks the regeneration instead. + Details: + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) + - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) + - Latest workflow run: ${RUN_URL} - **Click the link below to open a pre-filled PR:** + cc @iscai-msft @msyyc" + 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. + PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') + PR_BODY_RAW="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}" - 👉 [Create pull request from \`${BRANCH}\`](${COMPARE_URL}) + ISSUE_BODY="GitHub Actions is not permitted to create pull requests in this repository, so this issue tracks the regeneration instead. - Details: - - Source: [${SOURCE_LABEL}](${TS_REF_URL}) - - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) - - Latest workflow run: ${RUN_URL} + **Click the link below to open a pre-filled PR:** - cc @iscai-msft @msyyc" + 👉 [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 @iscai-msft @msyyc" + fi # Reuse an existing open tracking issue if one exists (matched by title). EXISTING_ISSUE=$(gh issue list --state open --search "\"$TITLE\" in:title" \ From 318af5441d01f249352d81c2ce79c0dc81018627 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 03:29:05 +0000 Subject: [PATCH 10/18] udpate assignee --- .../workflows/typespec-python-regenerate.yml | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 59d769509d5a..4f3a641af5da 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -184,6 +184,19 @@ jobs: SOURCE_LABEL="microsoft/typespec@main" fi + # 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}" # Check whether an open PR already exists from this branch to main. @@ -206,7 +219,7 @@ jobs: - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) - Latest workflow run: ${RUN_URL} - cc @iscai-msft @msyyc" + ${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 @@ -233,7 +246,7 @@ jobs: - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) - Latest workflow run: ${RUN_URL} - cc @iscai-msft @msyyc" + ${CC_LINE}" fi # Reuse an existing open tracking issue if one exists (matched by title). @@ -245,13 +258,13 @@ jobs: # Re-apply expected label and assignees in case they were removed from the existing issue. gh issue edit "$EXISTING_ISSUE" --body "$ISSUE_BODY" \ --add-label "typespec-python" \ - --add-assignee iscai-msft,msyyc + --add-assignee "$ASSIGNEES" ISSUE_NUMBER="$EXISTING_ISSUE" else echo "Creating new tracking issue" ISSUE_NUMBER=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ --label "typespec-python" \ - --assignee iscai-msft,msyyc \ + --assignee "$ASSIGNEES" \ --json number --jq '.number') echo "Created issue #$ISSUE_NUMBER" fi From dcb370a1af01396950b1e050cf4fff04fa85064d Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 03:37:32 +0000 Subject: [PATCH 11/18] add readme --- eng/tools/emitter/README.md | 48 +++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/eng/tools/emitter/README.md b/eng/tools/emitter/README.md index de1831a5f768..16b7e04fa501 100644 --- a/eng/tools/emitter/README.md +++ b/eng/tools/emitter/README.md @@ -4,7 +4,51 @@ 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. + - 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`). + + +### 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. From 59af2fc670337ba34b59763ffd8cbf476f4cda3e Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 03:49:25 +0000 Subject: [PATCH 12/18] add schedule --- .github/workflows/typespec-python-regenerate.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 4f3a641af5da..560d6b8e97c6 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -6,6 +6,9 @@ on: branches: [main] paths: - "eng/emitter-package.json" + # Run daily at 22:00 UTC against microsoft/typespec@main + schedule: + - cron: "0 22 * * *" # Allow manual triggering workflow_dispatch: inputs: From aab3256ee0820970d6525afe5d9e27196a9a1200 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 05:06:16 +0000 Subject: [PATCH 13/18] update --- .../workflows/typespec-python-regenerate.yml | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 560d6b8e97c6..09af85a4c841 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -151,6 +151,8 @@ jobs: 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" @@ -158,7 +160,27 @@ jobs: exit 0 fi - git checkout -B "$BRANCH" + # 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 From 07622bbb58347c1ce86b32d1e837286a4252da53 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 05:56:17 +0000 Subject: [PATCH 14/18] update --- .github/workflows/typespec-python-regenerate.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 09af85a4c841..e5485a61a817 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -287,11 +287,14 @@ jobs: ISSUE_NUMBER="$EXISTING_ISSUE" else echo "Creating new tracking issue" - ISSUE_NUMBER=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ + # `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`.) + ISSUE_URL=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ --label "typespec-python" \ - --assignee "$ASSIGNEES" \ - --json number --jq '.number') - echo "Created issue #$ISSUE_NUMBER" + --assignee "$ASSIGNEES") + ISSUE_NUMBER="${ISSUE_URL##*/}" + echo "Created issue #$ISSUE_NUMBER ($ISSUE_URL)" fi echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT From a1f06242e7e8838f18dd7b2b487e8f3988edc20d Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 06:01:38 +0000 Subject: [PATCH 15/18] update --- .../workflows/typespec-python-regenerate.yml | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index e5485a61a817..e3ca24797f24 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -310,14 +310,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'], + }); + } From c4761909214d76287b4802b5f4dadee665c36cc5 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 07:18:55 +0000 Subject: [PATCH 16/18] replace readme with template --- .../workflows/typespec-python-regenerate.yml | 17 +++++++++++++++++ eng/tools/emitter/gen/template/README.md | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 eng/tools/emitter/gen/template/README.md diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index e3ca24797f24..c9a213da275c 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -136,6 +136,23 @@ 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 + # - the template itself (gen/template/README.md) + find "$TARGET" -type f -name README.md \ + ! -path "$TARGET/README.md" \ + ! -path "$TEMPLATE" \ + -print -exec cp -f "$TEMPLATE" {} \; + - name: Commit and push changes id: push-changes 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. From 2606527cfacf099b82ef5711dce1e120b36bc5f0 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 08:03:23 +0000 Subject: [PATCH 17/18] udpate --- .../workflows/typespec-python-regenerate.yml | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index c9a213da275c..7ca6937748c4 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -241,6 +241,30 @@ jobs: TITLE="[typespec-python] Regenerate tests from ${SOURCE_LABEL}" + # Reuse an existing open tracking issue if one exists (matched by + # 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. + EXISTING_ISSUE=$(gh issue list --state open --search "\"$TITLE\" in:title" \ + --json number,title | jq -r --arg title "$TITLE" '.[] | select(.title == $title) | .number' | head -n1 || echo "") + + if [ -n "$EXISTING_ISSUE" ]; then + ISSUE_NUMBER="$EXISTING_ISSUE" + echo "Reusing existing tracking issue #$ISSUE_NUMBER" + else + 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 + # 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. @@ -266,8 +290,11 @@ jobs: # 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="Source: ${TS_REF_URL} + PR_BODY_RAW="Fixes ${ISSUE_LINK} + + Source: ${TS_REF_URL} Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. @@ -291,28 +318,11 @@ jobs: ${CC_LINE}" fi - # Reuse an existing open tracking issue if one exists (matched by title). - EXISTING_ISSUE=$(gh issue list --state open --search "\"$TITLE\" in:title" \ - --json number,title | jq -r --arg title "$TITLE" '.[] | select(.title == $title) | .number' | head -n1 || echo "") - - if [ -n "$EXISTING_ISSUE" ]; then - echo "Updating existing issue #$EXISTING_ISSUE" - # Re-apply expected label and assignees in case they were removed from the existing issue. - gh issue edit "$EXISTING_ISSUE" --body "$ISSUE_BODY" \ - --add-label "typespec-python" \ - --add-assignee "$ASSIGNEES" - ISSUE_NUMBER="$EXISTING_ISSUE" - else - 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`.) - ISSUE_URL=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ - --label "typespec-python" \ - --assignee "$ASSIGNEES") - ISSUE_NUMBER="${ISSUE_URL##*/}" - echo "Created issue #$ISSUE_NUMBER ($ISSUE_URL)" - 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: From 5ee52c4ddf3f602dd279cebf9df68b9001f79a28 Mon Sep 17 00:00:00 2001 From: Yuchao Yan Date: Thu, 14 May 2026 08:32:05 +0000 Subject: [PATCH 18/18] review --- .../workflows/typespec-python-regenerate.yml | 63 +++++++++++-------- eng/tools/emitter/README.md | 4 +- 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/.github/workflows/typespec-python-regenerate.yml b/.github/workflows/typespec-python-regenerate.yml index 7ca6937748c4..06aaecf79f2b 100644 --- a/.github/workflows/typespec-python-regenerate.yml +++ b/.github/workflows/typespec-python-regenerate.yml @@ -22,6 +22,10 @@ permissions: 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 @@ -147,10 +151,11 @@ jobs: fi # Replace every README.md under gen/ with the template, except: # - the top-level gen/README.md - # - the template itself (gen/template/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 "$TEMPLATE" \ + ! -path "$TARGET/template/*" \ -print -exec cp -f "$TEMPLATE" {} \; - name: Commit and push changes @@ -242,11 +247,19 @@ jobs: TITLE="[typespec-python] Regenerate tests from ${SOURCE_LABEL}" # Reuse an existing open tracking issue if one exists (matched by - # 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. - EXISTING_ISSUE=$(gh issue list --state open --search "\"$TITLE\" in:title" \ - --json number,title | jq -r --arg title "$TITLE" '.[] | select(.title == $title) | .number' | head -n1 || echo "") + # 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 "") if [ -n "$EXISTING_ISSUE" ]; then ISSUE_NUMBER="$EXISTING_ISSUE" @@ -276,16 +289,16 @@ jobs: 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}) + 👉 [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. + 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} + Details: + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) + - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) + - Latest workflow run: ${RUN_URL} - ${CC_LINE}" + ${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 @@ -294,28 +307,28 @@ jobs: PR_TITLE_ENC=$(jq -rn --arg t "$TITLE" '$t|@uri') PR_BODY_RAW="Fixes ${ISSUE_LINK} - Source: ${TS_REF_URL} + Source: ${TS_REF_URL} - Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. + Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. - - Workflow run: ${RUN_URL} + - Workflow run: ${RUN_URL} - This PR was auto-generated." + 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:** + **Click the link below to open a pre-filled PR:** - 👉 [Create pull request from \`${BRANCH}\`](${COMPARE_URL}) + 👉 [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} + Details: + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) + - Branch: [\`${BRANCH}\`](${SERVER}/${REPO}/tree/${BRANCH}) + - Latest workflow run: ${RUN_URL} - ${CC_LINE}" + ${CC_LINE}" fi # Write the final body onto the tracking issue (whether reused or diff --git a/eng/tools/emitter/README.md b/eng/tools/emitter/README.md index 16b7e04fa501..43af61d1a72e 100644 --- a/eng/tools/emitter/README.md +++ b/eng/tools/emitter/README.md @@ -38,12 +38,14 @@ The [`typespec-python-regenerate`](../../../.github/workflows/typespec-python-re - `[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. + - 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