Skip to content

Add language-agnostic emitter-diff tool + python adapter #6

Add language-agnostic emitter-diff tool + python adapter

Add language-agnostic emitter-diff tool + python adapter #6

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."