Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/http-client-python"
---

Add `--httpSpecsDir`, `--azureSpecsDir`, and `--no-baseline` flags to `regenerate.ts` so the language-agnostic `eng/emitter-diff` tool can drive code generation against pinned specs without cloning the published baseline.
205 changes: 205 additions & 0 deletions .github/workflows/ci-emitter-diff-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
name: "python / emitter diff"

# Generates code with the current checkout's emitter and with the PR's base
# commit, then diffs the two. The rendered HTML diff is uploaded as a per-PR
# artifact, and a sticky PR comment summarizes the changed files (+/-) with a
# link to download it, so reviewers can see exactly how an emitter change
# affects generated SDKs. Powered by the language-agnostic eng/emitter-diff tool.
#
# Python's generator uses a native two-phase pipeline (TypeSpec emits YAML, then
# a batched Python subprocess writes the .py files) with a venv co-located with
# each emitter version. So this workflow builds + sets up a venv for both the
# head checkout and a worktree of the baseline commit before diffing.

on:
pull_request:
branches:
- main
- release/*
paths:
- "packages/http-client-python/**"
- "eng/emitter-diff/**"
- ".github/workflows/ci-emitter-diff-python.yml"
workflow_dispatch:
inputs:
baseline:
description: "Baseline emitter ref (npm version, local path, or github ref). Defaults to the approved baseline SHA file."
required: false
default: ""

permissions:
contents: read
pull-requests: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
emitter-diff:
name: "Generate & Diff"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: ./.github/actions/setup

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install repo dependencies (emitter-diff tool)
run: pnpm install

- name: Build + venv for head emitter
working-directory: packages/http-client-python
run: |
npm ci
npm run build
npm run install

- name: Determine baseline
id: baseline
run: |
input="${{ github.event.inputs.baseline }}"
if [ -z "$input" ]; then
sha_file="eng/emitter-diff/baselines/python.sha"
base_sha="$(grep -vE '^[[:space:]]*(#|$)' "$sha_file" | head -n1 | tr -d '[:space:]')"
if [ -z "$base_sha" ]; then
echo "::error::No approved baseline SHA found in $sha_file"
exit 1
fi
echo "Approved baseline SHA: $base_sha (from $sha_file)"
git worktree add "${{ runner.temp }}/baseline" "$base_sha"
(cd "${{ runner.temp }}/baseline/packages/http-client-python" && npm ci && npm run build && npm run install)
input="local:${{ runner.temp }}/baseline/packages/http-client-python"
echo "sha=$base_sha" >> "$GITHUB_OUTPUT"
fi
echo "ref=$input" >> "$GITHUB_OUTPUT"
echo "Baseline: $input"

- name: Run emitter diff
id: diff
run: |
set +e
pnpm --filter @typespec/emitter-diff exec tsx src/cli.ts \
--emitter python \
--baseline "${{ steps.baseline.outputs.ref }}" \
--work-dir "${{ runner.temp }}/emitter-diff" \
--html "${{ runner.temp }}/emitter-diff.html" \
--patch "${{ runner.temp }}/emitter-diff.patch" \
--fail-on-diff \
| tee "${{ runner.temp }}/emitter-diff.log"
echo "status=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

- name: Write job summary
if: always()
run: |
{
echo "## Emitter diff"
echo ""
echo "Baseline (approved): \`${{ steps.baseline.outputs.sha }}\` vs current checkout."
echo ""
if [ "${{ steps.diff.outputs.status }}" = "2" ]; then
echo "❌ **Generated output changed vs the approved baseline.**"
echo ""
echo "If this change is intended, approve it by updating"
echo "\`eng/emitter-diff/baselines/python.sha\` to a commit on your branch that"
echo "contains your emitter changes, then push."
elif [ "${{ steps.diff.outputs.status }}" = "0" ]; then
echo "✅ Generated output matches the approved baseline."
fi
echo ""
echo '```'
grep -E "Diff summary:" "${{ runner.temp }}/emitter-diff.log" || echo "No summary captured."
echo '```'
echo ""
echo "Download the **emitter-diff-html** artifact for the full rendered diff."
} >> "$GITHUB_STEP_SUMMARY"

- name: Upload HTML diff
if: always()
uses: actions/upload-artifact@v7
with:
name: emitter-diff-html
path: ${{ runner.temp }}/emitter-diff.html
if-no-files-found: ignore
retention-days: 7

- name: Comment on PR
if: always() && github.event_name == 'pull_request'
continue-on-error: true
uses: actions/github-script@v7
env:
BASELINE: ${{ steps.baseline.outputs.sha }}
PATCH_FILE: ${{ runner.temp }}/emitter-diff.patch
HTML_FILE: ${{ runner.temp }}/emitter-diff.html
with:
script: |
const fs = require("fs");
const marker = "<!-- emitter-diff-python -->";
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

// Parse the unified patch directly so the numbers match the tool exactly.
let patch = "";
try { patch = fs.readFileSync(process.env.PATCH_FILE, "utf8"); } catch {}
const lines = patch.split("\n");
const files = [];
let insertions = 0, deletions = 0;
for (const line of lines) {
const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
if (m) files.push(m[2]);
else if (line.startsWith("+") && !line.startsWith("+++")) insertions++;
else if (line.startsWith("-") && !line.startsWith("---")) deletions++;
}
const hasChanges = patch.trim().length > 0;
const htmlExists = fs.existsSync(process.env.HTML_FILE);

let body = `${marker}\n## 🐍 Python emitter diff\n\n` +
`Approved baseline \`${process.env.BASELINE}\` vs this PR's checkout.\n\n`;
if (!hasChanges) {
body += `✅ **No changes** — generated output matches the approved baseline.\n`;
} else {
body += `❌ **${files.length} file(s) changed** · +${insertions} / -${deletions}\n\n`;
const shown = files.slice(0, 50);
body += `<details><summary>Changed files</summary>\n\n` +
shown.map((f) => `- \`${f}\``).join("\n") +
(files.length > shown.length ? `\n- …and ${files.length - shown.length} more` : "") +
`\n</details>\n\n`;
if (htmlExists) {
body += `📄 Download the **emitter-diff-html** artifact from the ` +
`[workflow run](${runUrl}) for the full side-by-side rendered diff.\n\n`;
}
body += `**This check fails until the change is approved.** If the new output is ` +
`intended, update \`eng/emitter-diff/baselines/python.sha\` to a commit on this ` +
`branch that contains your emitter changes, then push.\n`;
}
body += `\n_Generated by \`eng/emitter-diff\`._`;

const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number });
const existing = comments.find((c) => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number, body });
}

- name: Enforce baseline approval
if: always()
run: |
status="${{ steps.diff.outputs.status }}"
if [ "$status" = "2" ]; then
echo "::error::Generated output differs from the approved baseline (${{ steps.baseline.outputs.sha }})."
echo "Review the emitter-diff-html artifact. If the change is intended, approve it by"
echo "updating eng/emitter-diff/baselines/python.sha to a commit on your branch that"
echo "contains your emitter changes, then push."
exit 1
elif [ "$status" != "0" ]; then
echo "::error::emitter-diff failed (exit $status). See the log and artifact."
exit 1
fi
echo "Generated output matches the approved baseline."
1 change: 1 addition & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ words:
- getpgid
- ghapp
- giacamo
- gpgsign
- graalvm
- Gson
- Hdvcmxk
Expand Down
Loading
Loading