TypeSpec Python Regenerate Tests #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: TypeSpec Python Regenerate Tests | |
| on: | |
| # Trigger when eng/emitter-package.json is updated on main (uses default microsoft/typespec@main) | |
| push: | |
| 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: | |
| typespec_ref: | |
| 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" | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: true | |
| jobs: | |
| regenerate: | |
| name: "Regenerate TypeSpec Python tests" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout azure-sdk-for-python | |
| # SHA corresponds to actions/checkout@v6 | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve TypeSpec repo/ref | |
| id: typespec-info | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| 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 "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 | |
| # Checkout to "_typespec" (not "typespec") to avoid the workspace path | |
| # "azure-sdk-for-python" causing spec.includes("azure") to match all specs | |
| # in regenerate.ts, which breaks unbranded package name detection | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd | |
| with: | |
| repository: ${{ steps.typespec-info.outputs.typespec_repo }} | |
| ref: ${{ steps.typespec-info.outputs.typespec_ref }} | |
| path: _typespec | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| # SHA corresponds to actions/setup-node@v6 | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e | |
| with: | |
| node-version: lts/* | |
| - name: Setup Python | |
| # SHA corresponds to actions/setup-python@v5 | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 | |
| with: | |
| python-version: "3.12" | |
| - name: Build http-client-python | |
| working-directory: _typespec/packages/http-client-python | |
| run: | | |
| npm install --ignore-scripts | |
| npm run build | |
| - name: Prepare Python environment | |
| working-directory: _typespec/packages/http-client-python | |
| run: | | |
| npm run install | |
| npm run prepare | |
| - name: Regenerate tests | |
| working-directory: _typespec/packages/http-client-python | |
| run: | | |
| npm run regenerate | |
| - name: Copy regenerated tests | |
| run: | | |
| set -euo pipefail | |
| TARGET="eng/tools/emitter/gen" | |
| rm -rf "$TARGET/azure" "$TARGET/unbranded" | |
| mkdir -p "$TARGET" | |
| cp -r "_typespec/packages/http-client-python/tests/generated/azure" "$TARGET/azure" | |
| cp -r "_typespec/packages/http-client-python/tests/generated/unbranded" "$TARGET/unbranded" | |
| - name: Clean up typespec checkout | |
| run: rm -rf "_typespec" | |
| - name: Commit and push changes | |
| id: push-changes | |
| run: | | |
| set -euo pipefail | |
| 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" | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| # 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 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 | |
| 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 }}" | |
| # 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 | |
| # 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. | |
| # 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. | |
| 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}" | |
| 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 | |
| # 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" | |
| ISSUE_NUMBER=$(gh issue create --title "$TITLE" --body "$ISSUE_BODY" \ | |
| --label "typespec-python" \ | |
| --assignee "$ASSIGNEES" \ | |
| --json number --jq '.number') | |
| echo "Created issue #$ISSUE_NUMBER" | |
| fi | |
| echo "issue_number=$ISSUE_NUMBER" >> $GITHUB_OUTPUT | |
| notify-on-failure: | |
| name: "Notify on failure" | |
| needs: regenerate | |
| if: failure() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Send failure notification | |
| # SHA corresponds to actions/github-script@v7 | |
| uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b | |
| 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'] | |
| }); |