diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 68afe9b..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Build spec - -on: [pull_request, push] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: ljharb/actions/node/install@main - name: 'nvm install lts/* && npm install' - with: - node-version: lts/* - - run: npm run build diff --git a/.github/workflows/deploy.yml b/.github/workflows/publish-main.yml similarity index 68% rename from .github/workflows/deploy.yml rename to .github/workflows/publish-main.yml index 90d6c76..bf9776f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/publish-main.yml @@ -1,4 +1,4 @@ -name: Deploy gh-pages +name: Publish main on: push: @@ -6,9 +6,8 @@ on: - main jobs: - deploy: + publish: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - uses: ljharb/actions/node/install@main @@ -16,8 +15,10 @@ jobs: with: node-version: lts/* - run: npm run build - - uses: JamesIves/github-pages-deploy-action@v4 + - name: Publish to gh-pages + uses: JamesIves/github-pages-deploy-action@v4 with: branch: gh-pages folder: build - clean: true + clean-exclude: | + pr diff --git a/.github/workflows/publish-pr.yml b/.github/workflows/publish-pr.yml new file mode 100644 index 0000000..14399bc --- /dev/null +++ b/.github/workflows/publish-pr.yml @@ -0,0 +1,110 @@ +name: Publish PR +run-name: ${{ github.event.workflow_run.display_title }} + +on: + workflow_run: + workflows: ['Render PR'] + types: [completed] + +env: + # Must match ./render-pr.yml + ARTIFACT_NAME: ${{ vars.ARTIFACT_NAME || 'result' }} + +jobs: + publish: + runs-on: ubuntu-latest + if: > + ${{ + !github.event.repository.fork + && github.event.workflow_run.event == 'pull_request' + && github.event.workflow_run.conclusion == 'success' + }} + steps: + - uses: actions/checkout@v4 + - uses: ljharb/actions/node/install@main + name: 'nvm install lts/* && npm install' + with: + node-version: lts/* + - name: Print event info + uses: actions/github-script@v7 + with: + script: 'console.log(${{ toJson(github.event) }});' + - name: Download zipball + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const run_id = ${{ github.event.workflow_run.id }}; + const name = process.env.ARTIFACT_NAME; + const listArtifactsQuery = { owner, repo, run_id, name }; + const listArtifactsResponse = await github.rest.actions.listWorkflowRunArtifacts(listArtifactsQuery); + const { total_count, artifacts } = listArtifactsResponse.data; + if (total_count !== 1) { + const summary = artifacts?.map(({ name, size_in_bytes, url }) => ({ name, size_in_bytes, url })); + const detail = JSON.stringify(summary ?? []); + throw new RangeError(`Expected 1 ${name} artifact, got ${total_count} ${detail}`); + } + const artifact_id = artifacts[0].id; + console.log(`downloading artifact ${artifact_id}`); + const downloadResponse = await github.rest.actions.downloadArtifact({ + owner, + repo, + artifact_id, + archive_format: 'zip', + }); + const fs = require('fs'); + fs.writeFileSync('${{ github.workspace }}/result.zip', Buffer.from(downloadResponse.data)); + - name: Provide result directory + run: rm -rf result && mkdir -p result + - run: unzip -o result.zip -d result + - run: ls result + - name: Extract PR data + run: | + cd result + awk -v ok=1 ' + NR == 1 && match($0, /^[1-9][0-9]* [0-9a-fA-F]{7,}$/) { + print "PR=" $1; + print "COMMIT=" $2; + next; + } + { ok = 0; } + END { exit !ok; } + ' pr-data.txt >> "$GITHUB_ENV" + rm pr-data.txt + - name: Insert preview warning + run: | + tmp="$(mktemp -u XXXXXXXX.json)" + export REPO_URL="https://github.com/$GITHUB_REPOSITORY" + jq -n ' + def repo_link($args): $args as [$path, $contents] + | (env.REPO_URL + ($path // "")) as $url + | "\($contents // $url)"; + { + SUMMARY: "PR #\(env.PR)", + REPO_LINK: repo_link([]), + PR_LINK: repo_link(["/pull/" + env.PR, "PR #\(env.PR)"]), + COMMIT_LINK: ("commit " + repo_link(["/commit/" + env.COMMIT, "\(env.COMMIT)"])), + } + ' > "$tmp" + find result -name '*.html' -exec \ + node scripts/insert_warning.mjs scripts/pr_preview_warning.html "$tmp" '{}' '+' + - name: Publish to gh-pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + branch: gh-pages + folder: result + target-folder: pr/${{ env.PR }} + - name: Determine gh-pages url + id: get-pages-url + run: | + gh_pages_url="https://$(printf '%s' "$GITHUB_REPOSITORY" \ + | sed 's#/#.github.io/#; s#^tc39.github.io/#tc39.es/#')" + echo "url=$gh_pages_url" >> $GITHUB_OUTPUT + - name: Provide PR comment + uses: phulsechinmay/rewritable-pr-comment@v0.3.0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_ID: ${{ env.PR }} + message: > + The rendered spec for this PR is available at + ${{ steps.get-pages-url.outputs.url }}/pr/${{ env.PR }}. diff --git a/.github/workflows/render-pr.yml b/.github/workflows/render-pr.yml new file mode 100644 index 0000000..fc7537e --- /dev/null +++ b/.github/workflows/render-pr.yml @@ -0,0 +1,39 @@ +name: Render PR + +on: [pull_request] + +env: + # Must match ./publish-pr.yml + ARTIFACT_NAME: ${{ vars.ARTIFACT_NAME || 'result' }} + +jobs: + render: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request }} + steps: + - uses: actions/checkout@v4 + - name: '[node LTS] npm install' + uses: ljharb/actions/node/install@main + with: + node-version: lts/* + - run: npm run build + - name: Save PR data + env: + PR: ${{ github.event.number }} + run: echo "$PR $(git rev-parse --verify HEAD)" > build/pr-data.txt + - uses: actions/upload-artifact@v4 + id: upload + if: ${{ !github.event.repository.fork }} + with: + name: ${{ env.ARTIFACT_NAME }} + path: build/ + - name: Echo artifact ID + run: echo 'Artifact ID is ${{ steps.upload.outputs.artifact-id }}' + - name: Verify artifact discoverability + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const run_id = ${{ github.run_id }}; + const listArtifactsResponse = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, run_id }); + console.log(`artifacts for run id ${run_id}`, listArtifactsResponse?.data); diff --git a/package.json b/package.json index 438a1fa..e46c4c0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "license": "MIT", "devDependencies": { "@tc39/ecma262-biblio": "^2.1.2816", - "ecmarkup": "^20.0.0" + "ecmarkup": "^20.0.0", + "jsdom": "^25.0.1", + "parse5-html-rewriting-stream": "^7.0.0", + "tmp": "^0.2.3" }, "engines": { "node": ">= 18" diff --git a/scripts/insert_warning.mjs b/scripts/insert_warning.mjs new file mode 100644 index 0000000..f493d78 --- /dev/null +++ b/scripts/insert_warning.mjs @@ -0,0 +1,141 @@ +import fs from 'fs'; +import { readFile, rename } from 'fs/promises'; +import pathlib from 'path'; +import { pipeline } from 'stream/promises'; +import { parseArgs } from 'util'; +import { JSDOM } from 'jsdom'; +import { RewritingStream } from 'parse5-html-rewriting-stream'; +import tmp from 'tmp'; + +const { positionals: cliArgs } = parseArgs({ + allowPositionals: true, + options: {}, +}); +if (cliArgs.length < 3) { + const self = pathlib.relative(process.cwd(), process.argv[1]); + console.error(`Usage: node ${self} ... + +{{identifier}} substrings in template.html are replaced from data.json, then +the result is inserted into each file.html.`); + process.exit(64); +} + +const main = async ([templateFile, dataFile, ...files]) => { + // Substitute data into the template. + const [ + template, + data, + ] = await Promise.all([ + readFile(templateFile, 'utf8'), + readFile(dataFile, 'utf8').then(JSON.parse), + ]) + const formatErrors = []; + const placeholderPatt = /[{][{](?:([\p{ID_Start}$_][\p{ID_Continue}$]*)[}][}]|.*?(?:[}][}]|(?=[{][{])|$))/gsu; + const resolved = template.replaceAll(placeholderPatt, (m, name, i) => { + if (!name) { + const trunc = m.replace(/([^\n]{29}(?!$)|[^\n]{,29}(?=\n)).*/s, '$1…'); + formatErrors.push(new SyntaxError(`bad placeholder at index ${i}: ${trunc}`)); + } else if (!Object.hasOwn(data, name)) { + formatErrors.push(new ReferenceError(`no data for ${m}`)); + } + return data[name]; + }); + if (formatErrors.length > 0) { + throw new AggregateError(formatErrors); + } + + // Parse the template into DOM nodes for appending to page head (metadata such + // as + +
+ {{SUMMARY}} +

+ This document is a preview of merging {{PR_LINK}} as {{COMMIT_LINK}}. +

+

+ Do not reference it as authoritative in any way. + Instead, see the living specification at {{REPO_LINK}}. +

+
+