Skip to content

TypeSpec Python Regenerate Tests #14

TypeSpec Python Regenerate Tests

TypeSpec Python Regenerate Tests #14

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
pull-requests: 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"
echo "typespec_ref=$REF"
echo "typespec_display_ref=$DISPLAY_REF"
echo "typespec_ref_url=$REF_URL"
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: Stage regenerated tests
run: |
set -euo pipefail
GENERATED_STAGE_DIR="${RUNNER_TEMP}/typespec-python-generated"
rm -rf "$GENERATED_STAGE_DIR"
mkdir -p "$GENERATED_STAGE_DIR"
cp -r "_typespec/packages/http-client-python/tests/generated/azure" "$GENERATED_STAGE_DIR/azure"
cp -r "_typespec/packages/http-client-python/tests/generated/unbranded" "$GENERATED_STAGE_DIR/unbranded"
echo "GENERATED_STAGE_DIR=$GENERATED_STAGE_DIR" >> "$GITHUB_ENV"
- name: Checkout generated tests source branch
run: |
set -euo pipefail
TARGET_BRANCH="typespec-python-generated-tests"
PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}"
if [ -n "$PR_NUMBER" ]; then
SOURCE_BRANCH="regen/typespec-python-pr-${PR_NUMBER}"
else
SOURCE_BRANCH="regen/typespec-python-main"
fi
if ! git fetch --no-tags --depth=1 origin "$TARGET_BRANCH" 2>/dev/null; then
echo "::error::Branch $TARGET_BRANCH not found. Initialize it from the generated-tests history so eng/tools/azure-sdk-tools/emitter/generated/template/README.md is present before rerunning this workflow."
exit 1
fi
git checkout -B "$SOURCE_BRANCH" "origin/$TARGET_BRANCH"
- 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 "$GENERATED_STAGE_DIR/azure" "$TARGET/azure"
cp -r "$GENERATED_STAGE_DIR/unbranded" "$TARGET/unbranded"
- name: Clean up typespec checkout
run: |
rm -rf "_typespec"
if [ -n "${GENERATED_STAGE_DIR:-}" ]; then
rm -rf "$GENERATED_STAGE_DIR"
fi
- 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: Create or update tracking issue with PR link
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TARGET_BRANCH="typespec-python-generated-tests"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
GENERATED_DIR="eng/tools/azure-sdk-tools/emitter/generated"
# Quick check: skip if regeneration produced no changes vs HEAD.
if [ -z "$(git status --porcelain -- "$GENERATED_DIR")" ]; then
echo "No changes to commit"
exit 0
fi
PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}"
# Use a distinct source branch per origin so a main-based run and a
# PR-based run never force-push over each other.
if [ -n "$PR_NUMBER" ]; then
SOURCE_LABEL="microsoft/typespec PR #${PR_NUMBER}"
SOURCE_BRANCH="regen/typespec-python-pr-${PR_NUMBER}"
else
SOURCE_LABEL="microsoft/typespec@main"
SOURCE_BRANCH="regen/typespec-python-main"
fi
git add -f "$GENERATED_DIR"/
if git diff --cached --quiet; then
echo "No changes vs $TARGET_BRANCH"
exit 0
fi
COMMIT_MSG="[typespec-python] Regenerate tests from ${SOURCE_LABEL}"
git commit -m "$COMMIT_MSG"
git push origin "$SOURCE_BRANCH" --force-with-lease
# GitHub Actions' default GITHUB_TOKEN is not permitted to create pull
# requests in this repository, so instead of opening a PR directly we
# create/update a tracking issue containing a pre-filled "compare" link
# that a maintainer can click to open the PR manually.
REPO="${{ github.repository }}"
SERVER="${{ github.server_url }}"
RUN_URL="${SERVER}/${REPO}/actions/runs/${{ github.run_id }}"
TS_REF_URL="${{ steps.typespec-info.outputs.typespec_ref_url }}"
# Determine assignees. For manual (workflow_dispatch) triggers, assign
# to the user who triggered the run. For automatic triggers (push,
# schedule), 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="$COMMIT_MSG"
# Reuse an existing open tracking issue if one exists (matched by exact
# title). We list by label and match the title with jq because GitHub's
# search tokenizer strips characters like [ ] @ # and /, making a title
# search ambiguous.
ISSUES_JSON=$(gh issue list --state open --label typespec-python \
--limit 100 --json number,title)
EXISTING_ISSUE=$(jq -r --arg title "$TITLE" \
'first(.[] | select(.title == $title) | .number) // ""' <<< "$ISSUES_JSON")
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. The body is filled in below once we have
# the compare URL.
# Assignees are applied best-effort in the final `gh issue edit`
# below, so a non-assignable actor never fails issue creation.
ISSUE_URL=$(gh issue create --title "$TITLE" \
--body "Tracking issue for TypeSpec Python regeneration. Details will be filled in shortly." \
--label "typespec-python")
ISSUE_NUMBER="${ISSUE_URL##*/}"
echo "Created issue #$ISSUE_NUMBER ($ISSUE_URL)"
fi
# If an open PR already exists from the source branch to the target
# branch, point the tracking issue at it instead of asking for a new one.
EXISTING_PR_JSON=$(gh pr list --state open --head "$SOURCE_BRANCH" --base "$TARGET_BRANCH" \
--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 \`${SOURCE_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: [\`${SOURCE_BRANCH}\`](${SERVER}/${REPO}/tree/${SOURCE_BRANCH})
- Latest workflow run: ${RUN_URL}
> Note: the PR targets the \`${TARGET_BRANCH}\` branch, so merging it will **not** auto-close this issue. Please close this issue manually after the PR is merged.
${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. The PR is
# based on the dedicated target branch, not main.
ISSUE_LINK="${SERVER}/${REPO}/issues/${ISSUE_NUMBER}"
# For PR-sourced runs, mark the prefilled PR as do-not-merge: it only
# exists to surface the code diff of the upstream typespec PR.
PR_TITLE="$TITLE"
NOTE_PREFIX=""
DRAFT_PARAM=""
if [ -n "$PR_NUMBER" ]; then
PR_TITLE="$TITLE (DO NOT MERGE)"
NOTE_PREFIX=$'NOTE: This PR exists only to display the code diff. Please do not merge it.\n\n'
# PR-sourced regenerations open the prefilled PR as a draft
# (draft=1 is undocumented but honored by the quick_pull form).
DRAFT_PARAM="&draft=1"
fi
PR_TITLE_ENC=$(jq -rn --arg t "$PR_TITLE" '$t|@uri')
PR_BODY_RAW="${NOTE_PREFIX}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. Re-run the workflow to update it after new commits are pushed to the upstream TypeSpec PR"
PR_BODY_ENC=$(jq -rn --arg b "$PR_BODY_RAW" '$b|@uri')
COMPARE_URL="${SERVER}/${REPO}/compare/${TARGET_BRANCH}...${SOURCE_BRANCH}?quick_pull=1${DRAFT_PARAM}&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 \`${SOURCE_BRANCH}\`](${COMPARE_URL})
Details:
- Source: [${SOURCE_LABEL}](${TS_REF_URL})
- Branch: [\`${SOURCE_BRANCH}\`](${SERVER}/${REPO}/tree/${SOURCE_BRANCH})
- Latest workflow run: ${RUN_URL}
> Note: the PR targets the \`${TARGET_BRANCH}\` branch, so merging it will **not** auto-close this issue. Please close this issue manually after the PR is merged.
${CC_LINE}"
fi
# Write the final body onto the tracking issue (whether reused or just
# created) and re-apply the expected label. Assignment is best-effort:
# a non-assignable actor must not fail the workflow.
gh issue edit "$ISSUE_NUMBER" --body "$ISSUE_BODY" --add-label "typespec-python"
gh issue edit "$ISSUE_NUMBER" --add-assignee "$ASSIGNEES" \
|| echo "::warning::Could not assign issue #${ISSUE_NUMBER} to ${ASSIGNEES}"
echo "::notice::Tracking issue #${ISSUE_NUMBER} updated with PR compare link"
- name: Clean up stale regen branches
if: always()
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
TARGET_BRANCH="typespec-python-generated-tests"
PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}"
if [ -n "$PR_NUMBER" ]; then
CURRENT_BRANCH="regen/typespec-python-pr-${PR_NUMBER}"
else
CURRENT_BRANCH="regen/typespec-python-main"
fi
# Prune any regen/typespec-python-* branch whose PR (to the target
# branch) is no longer open but did exist at some point (merged or
# closed). Branches that never had a PR opened are left alone, since a
# maintainer may still click the pre-filled compare link to open one.
BRANCHES=$(git ls-remote --heads origin \
| sed 's#.*refs/heads/##' \
| grep '^regen/typespec-python-' || true)
for b in $BRANCHES; do
if [ "$b" = "$CURRENT_BRANCH" ]; then
continue
fi
OPEN_COUNT=$(gh pr list --state open --head "$b" --base "$TARGET_BRANCH" \
--json number --jq 'length')
ALL_COUNT=$(gh pr list --state all --head "$b" --base "$TARGET_BRANCH" \
--json number --jq 'length')
if [ "$OPEN_COUNT" = "0" ] && [ "$ALL_COUNT" != "0" ]; then
echo "Deleting stale branch $b (PR merged/closed, none open)"
git push origin --delete "$b" \
|| echo "::warning::Failed to delete branch $b"
fi
done
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'],
});
}