From 02a2848dbe86a7123b1e24a9ba89e957aa2e6d1a Mon Sep 17 00:00:00 2001 From: Krzysztof Rosinski Date: Wed, 14 Oct 2026 05:58:18 +0200 Subject: [PATCH 1/3] ci: parallelize browser verification and cancel superseded PR runs --- .github/workflows/ci.yml | 40 +++----- .github/workflows/release.yml | 30 +----- .github/workflows/verify.yml | 186 ++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 13 ++- docs/deployment.md | 19 ++-- 5 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/verify.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acec74f..60e0c87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,37 +11,25 @@ permissions: concurrency: group: smith-chart-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + verification: + uses: ./.github/workflows/verify.yml + with: + package-artifact: smithkit-package + upload-pages: true + check: + needs: verification + if: ${{ !cancelled() }} runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 2 steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/setup-node@v7 - with: - node-version-file: .nvmrc - cache: npm - - run: npm ci - - run: npx playwright install --with-deps chromium webkit firefox - - run: npm run prepare:release - - uses: actions/upload-pages-artifact@v5 - with: - path: dist/demo/ - - uses: actions/upload-artifact@v7 - with: - name: smithkit-package - path: artifacts/ - if-no-files-found: error - - uses: actions/upload-artifact@v7 - if: failure() - with: - name: browser-test-results - path: test-results/ - retention-days: 7 + - name: Require all verification jobs to pass + env: + VERIFICATION_RESULT: ${{ needs.verification.result }} + run: test "$VERIFICATION_RESULT" = success deploy: needs: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f320a0..d9e9adb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,31 +22,11 @@ jobs: if: >- github.repository == 'rosek86/smithkit' && (!inputs.release_tag || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ inputs.release_tag && format('refs/tags/{0}', inputs.release_tag) || github.event.release.tag_name || github.ref }} - persist-credentials: false - - uses: actions/setup-node@v7 - with: - node-version-file: .nvmrc - cache: npm - - name: Verify release version - if: github.event_name == 'release' || inputs.release_tag != '' - env: - RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }} - run: | - node --input-type=module -e "import fs from 'node:fs'; import assert from 'node:assert/strict'; const p = JSON.parse(fs.readFileSync('package.json')); assert.equal(process.env.RELEASE_TAG, 'v' + p.version); assert.match(p.version, /^\d+\.\d+\.\d+$/);" - - run: npm ci - - run: npx playwright install --with-deps chromium webkit firefox - - run: npm run prepare:release - - uses: actions/upload-artifact@v7 - with: - name: npm-package - path: artifacts/ - if-no-files-found: error + uses: ./.github/workflows/verify.yml + with: + ref: ${{ inputs.release_tag && format('refs/tags/{0}', inputs.release_tag) || github.event.release.tag_name || github.sha }} + release-tag: ${{ inputs.release_tag || github.event.release.tag_name || '' }} + package-artifact: npm-package publish: if: github.event_name == 'release' || inputs.release_tag != '' diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..976d189 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,186 @@ +name: Verify Smithkit + +on: + workflow_call: + inputs: + ref: + type: string + default: '' + release-tag: + type: string + default: '' + package-artifact: + type: string + required: true + upload-pages: + type: boolean + default: false + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + commit: ${{ steps.source.outputs.commit }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.ref || github.sha }} + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + - name: Record the exact source commit + id: source + run: | + test -z "$(git status --porcelain)" + echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Verify release version + if: inputs.release-tag != '' + env: + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + node --input-type=module -e "import fs from 'node:fs'; import assert from 'node:assert/strict'; const p = JSON.parse(fs.readFileSync('package.json')); assert.equal(process.env.RELEASE_TAG, 'v' + p.version); assert.match(p.version, /^\d+\.\d+\.\d+$/);" + - run: npm ci + - run: npm run check + - name: Pack the candidate once + run: | + mkdir -p artifacts + npm pack --ignore-scripts --pack-destination artifacts + test -z "$(git status --porcelain)" + - uses: actions/upload-artifact@v7 + with: + name: verification-build + path: | + dist/ + artifacts/*.tgz + if-no-files-found: error + retention-days: 1 + + browsers: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + browser: [chromium, webkit, firefox] + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.build.outputs.commit }} + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - uses: actions/download-artifact@v8 + with: + name: verification-build + - run: npx playwright install --with-deps ${{ matrix.browser }} + - name: Test the built demo + run: npx playwright test --browser=${{ matrix.browser }} --workers=2 + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: browser-test-results-${{ matrix.browser }} + path: test-results/ + retention-days: 7 + + consumers: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.build.outputs.commit }} + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - uses: actions/download-artifact@v8 + with: + name: verification-build + - run: npx playwright install --with-deps chromium webkit firefox + - name: Check the packed library + run: npm run check:package -- artifacts/*.tgz + - name: Check standalone consumers in all three browsers + run: npm run check:example -- artifacts/*.tgz + - name: Require unchanged source + run: test -z "$(git status --porcelain)" + + complete: + needs: [build, browsers, consumers] + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Require all verification jobs to pass + env: + BUILD_RESULT: ${{ needs.build.result }} + BROWSER_RESULT: ${{ needs.browsers.result }} + CONSUMER_RESULT: ${{ needs.consumers.result }} + run: | + test "$BUILD_RESULT" = success + test "$BROWSER_RESULT" = success + test "$CONSUMER_RESULT" = success + - uses: actions/checkout@v7 + with: + ref: ${{ needs.build.outputs.commit }} + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + - uses: actions/download-artifact@v8 + with: + name: verification-build + # Keep this self-contained so a dispatch can still verify older release tags. + - name: Record the verified archive + env: + VERIFIED_COMMIT: ${{ needs.build.outputs.commit }} + run: | + node --input-type=module <<'JS' + import assert from 'node:assert/strict'; + import { execFileSync } from 'node:child_process'; + import { createHash } from 'node:crypto'; + import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; + const manifest = JSON.parse(readFileSync('package.json', 'utf8')); + assert.equal(manifest.name, 'smithkit'); + assert.match(manifest.version, /^\d+\.\d+\.\d+$/); + const archives = readdirSync('artifacts').filter(name => name.endsWith('.tgz')); + assert.deepEqual(archives, [`smithkit-${manifest.version}.tgz`]); + const archive = `artifacts/${archives[0]}`; + const [packed] = JSON.parse(execFileSync('npm', ['pack', archive, '--dry-run', '--ignore-scripts', '--json'], { encoding: 'utf8' })); + assert.equal(packed.name, manifest.name); + assert.equal(packed.version, manifest.version); + const bytes = readFileSync(archive); + const integrity = `sha512-${createHash('sha512').update(bytes).digest('base64')}`; + assert.equal(packed.integrity, integrity); + const commit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); + assert.equal(commit, process.env.VERIFIED_COMMIT); + assert.equal(execFileSync('git', ['status', '--porcelain'], { encoding: 'utf8' }).trim(), ''); + const report = { + name: packed.name, version: packed.version, commit, archive: archives[0], + sha256: createHash('sha256').update(bytes).digest('hex'), integrity, + size: bytes.length, unpackedSize: packed.unpackedSize, + verified: ['source', 'chromium', 'webkit', 'firefox', 'package-consumer', 'standalone-example'], + }; + writeFileSync('artifacts/release-manifest.json', JSON.stringify(report, null, 2) + '\n'); + JS + - uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.package-artifact }} + path: artifacts/ + if-no-files-found: error + - uses: actions/upload-pages-artifact@v5 + if: inputs.upload-pages + with: + path: dist/demo/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aefcaf8..213a49b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,18 @@ Install all three engines first with `npx playwright install chromium webkit fir `PLAYWRIGHT_CHANNEL=chrome` can select installed Chrome for local checks; the script clears that override when running WebKit and Firefox. -CI and the release workflow use this same command. The release workflow also has +CI and the release workflow use `.github/workflows/verify.yml` to run the same +checks in parallel: build and pack once, then run a three-browser matrix alongside +package and standalone-consumer verification. Each job uses the recorded source +commit and downloads the same build artifact. Only after all jobs succeed does the +workflow write the release manifest and upload the verified package (and Pages +artifact for CI). The top-level `check` status remains the merge/deployment gate. + +A new push automatically cancels older runs of the same PR. Runs on `main` and npm +release workflows are not cancelled by newer runs. The local `prepare:release` +command retains its sequential execution and remains a complete verification path. + +The release workflow also has a manual dispatch: leave `release_tag` empty to verify only, or set an existing release tag on the default branch to retry its publication with the current workflow. diff --git a/docs/deployment.md b/docs/deployment.md index 9fafd12..acbd472 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -10,13 +10,18 @@ GitHub Pages serves the built demo from `dist/demo/`, not the source files on `m A push to `main` runs `.github/workflows/ci.yml`: -1. Install dependencies and Chromium/Firefox/WebKit. -2. Run `npm run prepare:release` to check the source, build the library and demo, - test all three browsers, and verify the packed library and standalone consumer. -3. Upload `dist/demo/` as the `github-pages` artifact. -4. Deploy that exact artifact with `actions/deploy-pages` after checks pass. - -Pull requests run the same checks and upload artifacts without deploying. +1. Call `.github/workflows/verify.yml` to run source checks, build the library and + demo, and pack one candidate archive. +2. Download that same build in parallel jobs: Chromium, WebKit, Firefox, and + package/standalone-consumer verification. All jobs check out the recorded commit. +3. Require every verification job to succeed, then create the release manifest + and upload `dist/demo/` as the `github-pages` artifact. +4. Require the top-level `check` gate, then deploy that exact artifact with + `actions/deploy-pages`. + +Pull requests run the same checks and upload artifacts without deploying. A new +push cancels older runs of that PR so obsolete checks do not hold up the latest +revision. Runs on `main` and release publication are not automatically cancelled. You can also select **Actions → Check and publish Smith chart → Run workflow** on `main` to retry a deployment. Runs on other branches cannot deploy. Preparing a release archive does not publish the npm package. From af8ac7e68807ce35c9b30ab437d10160edc6e19a Mon Sep 17 00:00:00 2001 From: Krzysztof Rosinski Date: Wed, 14 Oct 2026 05:59:03 +0200 Subject: [PATCH 2/3] fix: resolve the verification archive as a local npm path --- .github/workflows/verify.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 976d189..580f393 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -157,7 +157,7 @@ jobs: assert.match(manifest.version, /^\d+\.\d+\.\d+$/); const archives = readdirSync('artifacts').filter(name => name.endsWith('.tgz')); assert.deepEqual(archives, [`smithkit-${manifest.version}.tgz`]); - const archive = `artifacts/${archives[0]}`; + const archive = `./artifacts/${archives[0]}`; const [packed] = JSON.parse(execFileSync('npm', ['pack', archive, '--dry-run', '--ignore-scripts', '--json'], { encoding: 'utf8' })); assert.equal(packed.name, manifest.name); assert.equal(packed.version, manifest.version); From 964a1a6c9457d00669e966c506c67749181447cc Mon Sep 17 00:00:00 2001 From: Krzysztof Rosinski Date: Wed, 14 Oct 2026 05:59:46 +0200 Subject: [PATCH 3/3] docs: keep release archive instructions version independent --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 213a49b..5424c93 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ archive path after `--`, so release checks can reuse exactly the archive being s From a clean committed checkout, run `npm run prepare:release` for all source checks, all three demo browser suites, package-consumer checks, and the standalone example. It -writes `artifacts/smithkit-0.5.0.tgz` and `artifacts/release-manifest.json`, containing +writes `artifacts/smithkit-.tgz` and `artifacts/release-manifest.json`, containing the commit, SHA-256, npm integrity, and archive sizes. Nothing is published. Install all three engines first with `npx playwright install chromium webkit firefox`. `PLAYWRIGHT_CHANNEL=chrome` can select installed Chrome for local checks; the script