From 57538a88fc723b07123d3811e4608f0b9b2d1e0d Mon Sep 17 00:00:00 2001 From: Keith Miller Date: Tue, 11 Aug 2026 10:07:28 -0400 Subject: [PATCH] [ci] Render an upstream baseline copy of the spec in proposal repos Proposal repos are plain forks of `WebAssembly/spec`, so their published webassembly.github.io// site has no rendered copy of the unmodified spec to compare against. The proposals often also lag behind the upstream spec so a direct comparison between the rendered specs is full of false differences. A reviewer who wants to see what a proposal actually does to the spec text has to build upstream themselves, or read source diffs, which is particularly awkward for parts of the spec which are generated in the output. e.g. typeset rules, tables, appendices, etc. With this change a fork also renders every document at the commit where it diverged from its parent repo, and publishes the result under webassembly.github.io//upstream with the same layout as the main site. This will allow us to link to W3C's spec diff service and view a formatted and searchable comparison with a single URL. e.g. https://services.w3.org/htmldiff?doc1=https://webassembly.github.io//upstream/core/bikeshed/&doc2=https://webassembly.github.io//core/bikeshed/ The baseline is chosen by a new resolve-baseline job, which asks the `gh` API for the repository's parent, fetches the parent's main, and takes `git merge-base`. For most proposals which sync by merging upstream, this resolves to the most recent sync point. When running on `WebAssembly/spec`, a fork with no changes of its own, or when the parent cannot be determined / fetched, the job short-circuits and no upstream copy is generated. The commit upstream was rendered from is recorded at /upstream/baseline-sha and compared on the next run. When the shas match, the upstream variant is dropped from the build matrix and the published copy is carried over instead of being rendered again. This saves a decent amount of CI time for most (non-downstreaming) commits. --- .github/workflows/ci-spec.yml | 178 +++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-spec.yml b/.github/workflows/ci-spec.yml index 53a50ef56c..566d5b9b82 100644 --- a/.github/workflows/ci-spec.yml +++ b/.github/workflows/ci-spec.yml @@ -12,6 +12,11 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +# Path within the published site recording the commit the baseline was rendered +# from, both as provenance and so a run can tell the copy is still current. +env: + BASELINE_STAMP: upstream/baseline-sha + jobs: ensure-wasm-latest: if: ${{ github.repository == 'WebAssembly/spec' }} @@ -22,13 +27,84 @@ jobs: - name: Diff wasm-latest run: cd specification && bash diff-wasm-latest.sh + # Forks (i.e. proposal repos) also render the spec at the commit where they + # last diverged from their parent repo, so the proposal can be compared + # against an unmodified baseline. That baseline is published under /upstream. + resolve-baseline: + runs-on: ubuntu-latest + outputs: + variants: ${{ steps.resolve.outputs.variants }} + # The commit the baseline is rendered from, empty when there is none. A + # baseline absent from variants is one already published under /upstream. + base-sha: ${{ steps.resolve.outputs.base-sha }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + # merge-base needs the full history + fetch-depth: 0 + - name: Resolve upstream baseline + id: resolve + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + # Written once on whichever path exits below, so these defaults stand + # unless the checks further down replace them. + variants='["head"]' + base='' + write_outputs() { + echo "variants=$variants" >> "$GITHUB_OUTPUT" + echo "base-sha=$base" >> "$GITHUB_OUTPUT" + } + trap write_outputs EXIT + if [ "$REPOSITORY" = "WebAssembly/spec" ]; then + echo "Canonical repo, no upstream baseline." + exit 0 + fi + # A copy already on the site is left in place unless a newer baseline + # is rendered below. + published=$(gh api "repos/$REPOSITORY/contents/$BASELINE_STAMP?ref=gh-pages" \ + --jq .content 2>/dev/null | base64 -d | tr -d '[:space:]') || published='' + parent=$(gh api "repos/$REPOSITORY" --jq '.parent.full_name // ""' 2>/dev/null) || parent='' + if [ -z "$parent" ]; then + echo "::warning::Could not determine the parent repo, not rendering an upstream copy." + exit 0 + fi + if ! git fetch --no-tags --quiet "https://github.com/$parent.git" main; then + echo "::warning::Could not fetch $parent, not rendering an upstream copy." + exit 0 + fi + if ! merge_base=$(git merge-base HEAD FETCH_HEAD); then + echo "::warning::No common ancestor with $parent, no upstream copy." + exit 0 + fi + if [ "$merge_base" = "$(git rev-parse HEAD)" ]; then + echo "::notice::No divergence from $parent, no upstream copy." + exit 0 + fi + base=$merge_base + if [ "$base" = "$published" ]; then + echo "::notice::Baseline $base is already published, keeping that copy." + exit 0 + fi + echo "Upstream baseline: $base (from $parent)" + variants='["head","upstream"]' + build-core-spec: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 with: submodules: "recursive" + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Setup OCaml uses: ocaml/setup-ocaml@v3 with: @@ -54,14 +130,21 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: core-rendered + name: core-rendered-${{ matrix.variant }} path: document/core/_build/html build-js-api-spec: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 + with: + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Setup Bikeshed run: pip install bikeshed && bikeshed update - name: Run Bikeshed @@ -69,14 +152,21 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: js-api-rendered + name: js-api-rendered-${{ matrix.variant }} path: document/js-api/index.html build-web-api-spec: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 + with: + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Setup Bikeshed run: pip install bikeshed && bikeshed update - name: Run Bikeshed @@ -84,16 +174,22 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: web-api-rendered + name: web-api-rendered-${{ matrix.variant }} path: document/web-api/index.html build-code-metadata-spec: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 with: submodules: "recursive" + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Setup TexLive run: sudo apt-get update -y && sudo apt-get install -y latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended - name: Setup Sphinx @@ -103,16 +199,22 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: code-metadata-rendered + name: code-metadata-rendered-${{ matrix.variant }} path: document/metadata/code/_build/html build-legacy-exceptions-core-spec: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 with: submodules: "recursive" + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Setup TexLive run: sudo apt-get update -y && sudo apt-get install -y latexmk texlive-latex-recommended texlive-latex-extra texlive-fonts-recommended - name: Setup Sphinx @@ -122,14 +224,21 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: legacy-exceptions-core-rendered + name: legacy-exceptions-core-rendered-${{ matrix.variant }} path: document/legacy/exceptions/core/_build/html build-legacy-exceptions-js-api-spec: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 + with: + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Setup Bikeshed run: pip install bikeshed && bikeshed update - name: Run Bikeshed @@ -137,24 +246,32 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: legacy-exceptions-js-api-rendered + name: legacy-exceptions-js-api-rendered-${{ matrix.variant }} path: document/legacy/exceptions/js-api/index.html build-spec-versions: runs-on: ubuntu-latest + needs: resolve-baseline + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.resolve-baseline.outputs.variants) }} steps: - name: Checkout repo uses: actions/checkout@v4 + with: + ref: ${{ matrix.variant == 'upstream' && needs.resolve-baseline.outputs.base-sha || '' }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: versions-rendered + name: versions-rendered-${{ matrix.variant }} path: document/versions/ publish-spec: runs-on: ubuntu-latest needs: - ensure-wasm-latest + - resolve-baseline - build-core-spec - build-js-api-spec - build-web-api-spec @@ -165,43 +282,76 @@ jobs: steps: - name: Checkout repo uses: actions/checkout@v4 + with: + # The baseline's index.html is read out of the history below + fetch-depth: 0 - name: Create output directory run: mkdir _output && cp document/index.html _output/index.html - name: Download core spec artifact uses: actions/download-artifact@v4 with: - name: core-rendered + name: core-rendered-head path: _output/core - name: Download JS API spec artifact uses: actions/download-artifact@v4 with: - name: js-api-rendered + name: js-api-rendered-head path: _output/js-api - name: Download Web API spec artifact uses: actions/download-artifact@v4 with: - name: web-api-rendered + name: web-api-rendered-head path: _output/web-api - name: Download code metadata spec artifact uses: actions/download-artifact@v4 with: - name: code-metadata-rendered + name: code-metadata-rendered-head path: _output/metadata/code - name: Download legacy exceptions core spec artifact uses: actions/download-artifact@v4 with: - name: legacy-exceptions-core-rendered + name: legacy-exceptions-core-rendered-head path: _output/legacy/exceptions/core - name: Download legacy exceptions JS API spec artifact uses: actions/download-artifact@v4 with: - name: legacy-exceptions-js-api-rendered + name: legacy-exceptions-js-api-rendered-head path: _output/legacy/exceptions/js-api - name: Download spec versions artifacts uses: actions/download-artifact@v4 with: - name: versions-rendered + name: versions-rendered-head path: _output/versions + - name: Download upstream baseline artifacts + if: contains(fromJSON(needs.resolve-baseline.outputs.variants), 'upstream') + uses: actions/download-artifact@v4 + with: + pattern: '*-rendered-upstream' + path: _upstream + - name: Assemble upstream baseline copy + if: contains(fromJSON(needs.resolve-baseline.outputs.variants), 'upstream') + env: + BASE_SHA: ${{ needs.resolve-baseline.outputs.base-sha }} + run: | + set -euo pipefail + mkdir -p _output/upstream/metadata _output/upstream/legacy/exceptions + git show "$BASE_SHA:document/index.html" > _output/upstream/index.html + mv _upstream/core-rendered-upstream _output/upstream/core + mv _upstream/js-api-rendered-upstream _output/upstream/js-api + mv _upstream/web-api-rendered-upstream _output/upstream/web-api + mv _upstream/code-metadata-rendered-upstream _output/upstream/metadata/code + mv _upstream/legacy-exceptions-core-rendered-upstream _output/upstream/legacy/exceptions/core + mv _upstream/legacy-exceptions-js-api-rendered-upstream _output/upstream/legacy/exceptions/js-api + mv _upstream/versions-rendered-upstream _output/upstream/versions + echo "$BASE_SHA" > "_output/$BASELINE_STAMP" + - name: Keep the upstream baseline copy already published + if: needs.resolve-baseline.outputs.base-sha != '' && !contains(fromJSON(needs.resolve-baseline.outputs.variants), 'upstream') + run: | + set -euo pipefail + # Publishing replaces the whole site, so the copy has to be carried + # over explicitly rather than just left in place. + git fetch --no-tags --depth 1 origin gh-pages + git archive FETCH_HEAD upstream | tar -x -C _output - name: Publish to GitHub Pages if: github.ref == 'refs/heads/main' uses: peaceiris/actions-gh-pages@v4