Skip to content
Merged
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:
- "@azure-tools/typespec-go"
---

Add `diff-regen-code` script wiring typespec-go into the language-agnostic `emitter-diff` tool to diff all generated test output (`test/http-specs`, `test/azure-http-specs`, and `test/local`) between emitter versions.
153 changes: 153 additions & 0 deletions .github/workflows/ci-emitter-diff-go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: "go / emitter diff"

# Diffs generated code between this PR's typespec-go emitter and the merge-base
# baseline. Informational only: reports a sticky PR comment + HTML artifact, and
# fails the job only on a tool/build error (never on a diff). See
# core/eng/emitter-diff.
#
# The tool runs `npm run tspcompile` verbatim in each tree. It auto-prepares the
# baseline tree it fetches from GitHub (via the script's --setup: submodule init
# + install + build), so this workflow only builds the head (PR) emitter itself.

on:
pull_request:
branches: [main, release/*]
paths:
- "packages/typespec-go/**"
- "core/eng/emitter-diff/**"
- ".github/workflows/ci-emitter-diff-go.yml"
workflow_dispatch:
inputs:
baseline:
description: "Baseline emitter ref (local path or github ref). Defaults to the PR merge-base. A gh:/github: ref is fetched and auto-prepared (submodule init + install + build) by the tool; a local: path is used as-is."
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
# Fork PRs are intentionally skipped
if: github.event.pull_request.head.repo.fork != true
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive

- uses: ./.github/actions/setup

- name: Use the azure-sdk npm registry
# Point npm/pnpm at the azure-sdk public upstream feed (a reliable
# npmjs mirror, same registry the ADO pipelines use) for every install
# in this job. Written to $HOME/.npmrc so it is inherited not only by
# the repo install below but also by the baseline tree the emitter-diff
# tool fetches into a temp dir and installs via --setup (that tree is
# outside the repo and would otherwise miss a repo-local .npmrc). This
# job does two full installs, so a reliable registry matters more here.
run: echo "registry=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/" >> "$HOME/.npmrc"

- name: Install repo dependencies
run: pnpm install

- name: Determine baseline
id: baseline
# Dispatch input is untrusted: pass via env, reference only as "$VAR".
env:
BASELINE_INPUT: ${{ github.event.inputs.baseline || '' }}
BASE_REF: ${{ github.event.pull_request.base.ref || github.event.repository.default_branch }}
run: |
input="$BASELINE_INPUT"
if [ -n "$input" ]; then
echo "ref=$input" >> "$GITHUB_OUTPUT"
echo "Baseline (explicit): $input"
else
# merge-base = a real commit on the base branch; survives squash/rebase.
git fetch --no-tags origin "$BASE_REF"
base_sha="$(git merge-base FETCH_HEAD HEAD)"
[ -n "$base_sha" ] || { echo "::error::No merge-base with $BASE_REF."; exit 1; }
# gh:<sha> = this repo at the merge-base. The tool fetches that tree
# and auto-preps it (submodule init + install + build) via --setup.
echo "ref=gh:$base_sha" >> "$GITHUB_OUTPUT"
echo "Baseline (merge-base): gh:$base_sha"
fi

- name: Prepare head emitter (build)
# The tool runs `npm run tspcompile` against the current working tree
# as-is; build the head emitter (and its workspace deps) first. The
# baseline tree the tool fetches is prepped by the script's --setup.
# Matches the Build step in ci-go.yml.
run: pnpm --filter "@azure-tools/typespec-go" run build:deps

- name: Run emitter diff
id: diff
working-directory: packages/typespec-go
env:
BASELINE_REF: ${{ steps.baseline.outputs.ref }}
RUNNER_TEMP: ${{ runner.temp }}
run: |
set +e
# No --fail-on-diff (informational); tool/build errors still exit non-zero
# (checked in "Fail on tool error").
npm run diff-regen-code -- \
--ci \
--baseline "$BASELINE_REF" \
--html "$RUNNER_TEMP/emitter-diff.html" \
--md "$RUNNER_TEMP/emitter-diff.md" \
| tee "$RUNNER_TEMP/emitter-diff.log"
echo "status=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
# Reuse the tool's own summary line (strip ANSI) instead of re-parsing.
summary="$(sed -r 's/\x1b\[[0-9;]*m//g' "$RUNNER_TEMP/emitter-diff.log" \
| grep -oE 'Diff summary: [0-9]+ file\(s\), \+[0-9]+ / -[0-9]+' | head -1)"
echo "summary=${summary:-No changes to generated output.}" >> "$GITHUB_OUTPUT"
# Render the diff inline on the run page.
if [ -f "$RUNNER_TEMP/emitter-diff.md" ]; then
cat "$RUNNER_TEMP/emitter-diff.md" >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload HTML diff
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: emitter-diff-go-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@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BASELINE: ${{ steps.baseline.outputs.sha || steps.baseline.outputs.ref }}
STATUS: ${{ steps.diff.outputs.status }}
SUMMARY: ${{ steps.diff.outputs.summary }}
with:
script: |
const marker = "<!-- emitter-diff-go -->";
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const { STATUS, SUMMARY, BASELINE } = process.env;
let body = `${marker}\n### Go emitter diff\nBaseline \`${BASELINE}\` vs this PR.\n\n`;
body += STATUS !== "0"
? `⚠️ **emitter-diff failed** (exit \`${STATUS}\`) — tool/build error, not a diff. See the [run](${runUrl}).\n`
: `${SUMMARY}\n\nRendered diff: inline on the [run summary](${runUrl}), or the **emitter-diff-go-html** artifact.\n`;
body += `\n_Informational check (core/eng/emitter-diff); does not block the PR._`;
const { owner, repo } = context.repo, issue_number = context.issue.number;
const { data } = await github.rest.issues.listComments({ owner, repo, issue_number });
const hit = data.find((c) => c.body && c.body.includes(marker));
if (hit) await github.rest.issues.updateComment({ owner, repo, comment_id: hit.id, body });
else await github.rest.issues.createComment({ owner, repo, issue_number, body });

- name: Fail on tool error
if: always()
env:
STATUS: ${{ steps.diff.outputs.status }}
run: '[ "$STATUS" = "0" ] || { echo "::error::emitter-diff failed (exit $STATUS)."; exit 1; }'
20 changes: 20 additions & 0 deletions packages/typespec-go/docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This guide outlines the steps to contributing to the emitter.
- [Choosing where to add a test](#choosing-where-to-add-a-test)
- [Write an emitter unit test (scenario)](#write-an-emitter-unit-test-scenario)
- [Regenerate the Go fixtures](#regenerate-the-go-fixtures)
- [Diff the regenerated code](#diff-the-regenerated-code)
- [Run the Go tests](#run-the-go-tests)
- [Lint the generated Go](#lint-the-generated-go)
- [Debug](#debug)
Expand Down Expand Up @@ -132,6 +133,25 @@ pnpm tspcompile --filter=TestName

The first run also syncs the Azure REST API specs into `temp/`. The generated Go fixtures are not committed (they are `.gitignore`d), so validate your change by running the Go tests and linter below rather than by diffing the fixtures.

### Diff the regenerated code

Because the fixtures are not committed, a plain `git diff` won't show how your emitter change affects the generated Go. To review that impact, `pnpm diff-regen-code` regenerates the code twice — once from a **baseline** emitter and once from your working tree (**head**) — and renders the difference as a clickable HTML report plus a terminal summary. It wraps the language-agnostic [`emitter-diff`](https://github.com/microsoft/typespec/tree/main/eng/emitter-diff) tool that lives in the `core` submodule, running `pnpm tspcompile` in each tree and diffing `test/http-specs`, `test/azure-http-specs`, and `test/local`.

Build your working tree first (`pnpm build:deps` above) — head is run as-is and never rebuilt for you. The baseline defaults to `main` and is fetched from GitHub and prepared (submodule init, install, build) automatically. From the `packages/typespec-go` directory:

```terminal
pnpm diff-regen-code
```

Everything after a `--` is forwarded to the tool; a second `--` forwards to `tspcompile`, so you can diff a subset or pick a different baseline. For example, to diff only one test, or against a specific commit:

```terminal
pnpm diff-regen-code -- -- --filter=TestName
pnpm diff-regen-code -- --baseline gh:<sha>
```

This is the same tool the `go / emitter diff` PR check runs to post an informational diff of your emitter change; it never fails on a diff, only on a build/tool error.

### Run the Go tests

The Spector-backed specs under `test/http-specs/` and `test/azure-http-specs/` exercise the [Spector](https://github.com/microsoft/typespec/tree/main/packages/spector) mock server, while the local Go tests under `test/local/` (for example `fakeserver` and `gogenerate`) run standalone. All of them run against the fixtures produced by [Regenerate the Go fixtures](#regenerate-the-go-fixtures), so regenerate first. Start the mock server, run the tests, then stop it:
Expand Down
1 change: 1 addition & 0 deletions packages/typespec-go/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"tspcompile": "node .scripts/tspcompile.js",
"spector": "node .scripts/spector.js",
"sync-specs": "node .scripts/sync-azure-rest-api-specs.js",
"diff-regen-code": "node ../../core/eng/emitter-diff/src/cli.ts --command \"npm run tspcompile\" --emitter-path packages/typespec-go --generated-code-path test/http-specs,test/azure-http-specs,test/local --setup \"git submodule update --init --recursive\" --setup \"pnpm install\" --setup \"npm run build:deps\"",
"regen-docs": "tspd doc . --enable-experimental --output-dir ../../website/src/content/docs/docs/emitters/clients/typespec-go/reference --skip-js"
},
"files": [
Expand Down
Loading