Skip to content

TypeSpec Python Regenerate Tests #13

TypeSpec Python Regenerate Tests

TypeSpec Python Regenerate Tests #13

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
# 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/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: 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
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}"
# 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 #<N>" 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"
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.
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"
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'],
});
}