TypeSpec Python Regenerate Tests #21
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 | |
| 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 | |
| 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/azure-sdk-tools/emitter/generated" | |
| 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: Apply README template to generated test packages | |
| run: | | |
| set -euo pipefail | |
| TARGET="eng/tools/azure-sdk-tools/emitter/generated" | |
| 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 generated/ with the template, except: | |
| # - the top-level generated/README.md | |
| # - anything under generated/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 to dedicated branch | |
| run: | | |
| set -euo pipefail | |
| BRANCH="typespec-python-generated-tests" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Quick check: skip if regeneration produced no changes vs HEAD. | |
| if [ -z "$(git status --porcelain -- eng/tools/azure-sdk-tools/emitter/generated/)" ]; then | |
| echo "No changes to commit" | |
| exit 0 | |
| fi | |
| # Push regenerated files directly to the dedicated branch. | |
| # This branch is machine-managed and may be force-pushed. | |
| PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}" | |
| if [ -n "$PR_NUMBER" ]; then | |
| SOURCE_LABEL="microsoft/typespec PR #${PR_NUMBER}" | |
| else | |
| SOURCE_LABEL="microsoft/typespec@main" | |
| fi | |
| # Base on origin/main so the dedicated branch never inherits | |
| # unrelated content from whatever ref the workflow checked out. | |
| git fetch --no-tags --depth=1 origin main | |
| git checkout -B "$BRANCH" origin/main | |
| # Re-apply just the regenerated tree on top of origin/main. | |
| git checkout HEAD@{1} -- eng/tools/azure-sdk-tools/emitter/generated | |
| git add -f eng/tools/azure-sdk-tools/emitter/generated/ | |
| if git diff --cached --quiet; then | |
| echo "No changes vs origin/main" | |
| exit 0 | |
| fi | |
| git commit -m "[typespec-python] Regenerate tests from ${SOURCE_LABEL}" | |
| git push origin "$BRANCH" --force-with-lease | |
| echo "::notice::Pushed regenerated tests to $BRANCH" | |
| 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}`; | |
| 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'], | |
| }); | |
| } |