Skip to content

TypeSpec Python Regenerate Tests #22

TypeSpec Python Regenerate Tests

TypeSpec Python Regenerate Tests #22

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'],
});
}