Repository navigation
chore(deps): update dependency chrome-for-testing to v157 #971
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "Release: publish (GitHub Release + sign + tag)" | |
| # knope's "publish" half. When the `chore: prepare release X.Y.Z` PR (opened by | |
| # knope-prepare.yml from the `release` branch) merges, this workflow builds the | |
| # standalone binaries, builds + Sigstore-signs the dists and binaries, generates | |
| # SLSA provenance, and stages the whole signed set into dist/release-assets/. Only | |
| # then does `knope release` run: with `assets` configured it creates the GitHub | |
| # Release as a DRAFT, uploads every asset, and un-drafts (publishes) — the git tag | |
| # is created at publish, so the release locks immutable WITH its assets (#98). | |
| # Publishing via the clauster-ci App token makes the resulting tag trigger | |
| # release.yml (PyPI + GHCR). | |
| # | |
| # Triggered on the release PR MERGING (pull_request: closed + merged + the head | |
| # branch is `release`), NOT on a commit-message marker — the repo squash-merges | |
| # and a squash subject is editable, so keying publish off it risked a silent | |
| # never-publish. The head_ref is reliable regardless of the squash subject. Because | |
| # the squash orphans the PR-head commit, we check out the squash commit on main | |
| # (merge_commit_sha) so knope tags the commit that actually landed. | |
| # | |
| # FAIL CLOSED: if any dist, target binary, signature, or the provenance bundle is | |
| # missing, the gate aborts BEFORE `knope release` — leaving no tag, no PyPI/Docker, | |
| # nothing published. A half-built release never reaches the publish step. | |
| on: | |
| pull_request: | |
| types: [closed] | |
| branches: [main] | |
| # Manual fallback if a publish half-fails and is finished by hand. Gated to main | |
| # below; dispatch from main once pyproject.toml already holds the version. | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # Releases must NOT be cancelled mid-flight. | |
| group: knope-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # --- Build the standalone binaries (only on a merged `release` PR) ---------- | |
| # Reuses the validated build + smoke matrix (build-binaries.yml). When this job | |
| # is skipped (any other closed PR), every downstream job is skipped too. | |
| binaries: | |
| if: >- | |
| ${{ (github.event_name == 'pull_request' | |
| && github.event.pull_request.merged == true | |
| && github.head_ref == 'release') | |
| || (github.event_name == 'workflow_dispatch' | |
| && github.ref == 'refs/heads/main') }} | |
| permissions: | |
| contents: read | |
| uses: $/.github/workflows/build-binaries.yml | |
| # --- Build the dists in a job with NO signing identity ---------------------- | |
| # ⚠️ Deliberately NOT part of the `release` job below. `uv build` executes the | |
| # build backend and any build-time hooks in the dependency tree; the release job | |
| # holds `id-token: write` for Sigstore keyless signing, so building there would | |
| # put arbitrary code beside an OIDC identity that can sign artifacts in this | |
| # repository's name. The dists cross as an artifact instead. Same reasoning as | |
| # release.yml's pypi-build/pypi split. | |
| # | |
| # `needs: binaries` is for the GATE, not for data: it inherits the merged-release-PR | |
| # condition above by skip-propagation rather than duplicating that expression, which | |
| # would then be two places to keep in sync. `uv build` is seconds against a five-target | |
| # binary matrix, so the serialisation costs effectively nothing. | |
| dists: | |
| needs: [binaries] | |
| if: ${{ needs.binaries.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out the release commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} | |
| persist-credentials: false | |
| - name: Set up uv (build) | |
| uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 | |
| with: | |
| version: "0.13.7" # pinned uv (Renovate-tracked); skip the flaky "latest" fetch | |
| enable-cache: false # a poisoned cache would taint what the release job signs | |
| - name: Build sdist + wheel | |
| run: uv build | |
| - name: Upload dists for the release job | |
| uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 | |
| with: | |
| name: release-dists | |
| path: dist/ | |
| if-no-files-found: error # an empty upload would trip the fail-closed gate later | |
| # --- Sign + stage every asset, fail-closed, then publish via knope ---------- | |
| release: | |
| needs: [binaries, dists] | |
| # Explicit (not just needs-skip-propagation): publish only if BOTH inputs | |
| # actually built — the binaries matrix and the dists. Guards against a future | |
| # edit that adds `if: always()`, under which an unlisted dependency would go | |
| # unchecked and this job would run with an empty dist/. | |
| if: ${{ needs.binaries.result == 'success' && needs.dists.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| id-token: write # Sigstore keyless signing (OIDC) | |
| attestations: write # actions/attest records SLSA build provenance | |
| steps: | |
| # Check out the squash commit that landed on main (merge_commit_sha), NOT the | |
| # orphaned PR-head — knope must tag a commit that is actually on main. Full | |
| # history + tags so knope can compare the bumped version against the last | |
| # release tag and load the matching CHANGELOG section as the release notes. | |
| - name: Check out the release commit | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }} | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| # Pull the dists built by the `dists` job. This job does NOT build — see the | |
| # note on that job: it holds the Sigstore OIDC identity, so nothing here may | |
| # execute the build backend or its hooks. | |
| - name: Download the built dists | |
| uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 | |
| with: | |
| name: release-dists | |
| path: dist | |
| # Pull the binaries built by the `binaries` job into ./binaries (flattened): | |
| # each artifact carries the renamed binary + its .sha256. | |
| - name: Download the standalone binaries | |
| uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 | |
| with: | |
| pattern: clauster-binary-* | |
| path: binaries | |
| merge-multiple: true | |
| # Replace the per-target .sha256 files with a single SHA256SUMS over the | |
| # binaries, and assert all five expected targets are present BEFORE signing — | |
| # a missing target must abort, never silently ship a partial set. | |
| - name: Collect binaries + checksums | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cd binaries | |
| rm -f ./*.sha256 | |
| shopt -s nullglob | |
| bins=(clauster-*) | |
| for want in linux-x86_64 linux-arm64 macos-arm64 macos-x86_64 windows-x86_64; do | |
| match=0 | |
| for b in "${bins[@]}"; do case "$b" in *"$want"*) match=1 ;; esac; done | |
| [ "$match" -eq 1 ] || { echo "missing binary for target '$want'; aborting" >&2; exit 1; } | |
| done | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "${bins[@]}" > SHA256SUMS | |
| else | |
| shasum -a 256 "${bins[@]}" > SHA256SUMS | |
| fi | |
| echo "collected ${#bins[@]} binaries + SHA256SUMS" | |
| # SLSA build provenance over the dists, binaries, and checksum manifest. MUST | |
| # run BEFORE the signing step: the `binaries/clauster-*` glob would otherwise | |
| # also match the `.sigstore.json` bundles Sigstore writes next to each binary, | |
| # enumerating signature files as provenance subjects. Attesting first — while | |
| # only the real artifacts exist — keeps the subject set to exactly the dists + | |
| # binaries + SHA256SUMS. | |
| - name: Generate SLSA build provenance | |
| id: provenance | |
| uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 | |
| with: | |
| subject-path: "dist/*.tar.gz,dist/*.whl,binaries/clauster-*,binaries/SHA256SUMS" | |
| # Sigstore-sign the dists, the binaries, and the checksum manifest. Sigstore | |
| # v3 writes a .sigstore.json bundle next to each input and exits non-zero on | |
| # failure, so a signing error aborts before publish. | |
| - name: Sign artifacts with Sigstore | |
| uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0 | |
| with: | |
| inputs: >- | |
| ./dist/*.tar.gz | |
| ./dist/*.whl | |
| ./binaries/clauster-* | |
| ./binaries/SHA256SUMS | |
| # Stage the full signed set into dist/release-assets/ (knope's `assets` glob) | |
| # and FAIL CLOSED if anything is missing — knope only errors on a globbed file | |
| # that vanished, not on a target that never got built, so this gate is what | |
| # guarantees the published release is complete. | |
| - name: Stage signed assets + fail-closed gate | |
| shell: bash | |
| env: | |
| PROVENANCE_BUNDLE: ${{ steps.provenance.outputs.bundle-path }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| # Authoritative version from [project] (tomllib, not a column-0 grep that a | |
| # [tool.*] version line could shadow). | |
| ver_py='import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])' | |
| VERSION="$(python3 -c "$ver_py")" | |
| if [ -z "${VERSION}" ]; then | |
| echo "could not read version from pyproject.toml; aborting" >&2; exit 1 | |
| fi | |
| # Provenance: name it to match the other release artifacts. Scorecard's | |
| # Signed-Releases provenance probe matches on the `.intoto.jsonl` SUFFIX, | |
| # so the prefix is free; keep the `multiple` token (multi-subject bundle). | |
| if [ -z "${PROVENANCE_BUNDLE:-}" ] || [ ! -s "${PROVENANCE_BUNDLE}" ]; then | |
| echo "no SLSA provenance bundle produced; refusing to publish without provenance" >&2 | |
| exit 1 | |
| fi | |
| provenance="dist/clauster-${VERSION}-multiple.intoto.jsonl" | |
| cp "${PROVENANCE_BUNDLE}" "${provenance}" | |
| # Build the asset set from the clean source dirs (dist/, binaries/), where | |
| # the dist vs binary signatures don't glob-collide. | |
| dists=(dist/*.tar.gz dist/*.whl) | |
| dist_sigs=(dist/*.sigstore dist/*.sigstore.json) | |
| bins=(binaries/clauster-*) | |
| # Split the binary list: the glob also matches the per-binary .sigstore.json. | |
| bin_files=(); bin_sigs=() | |
| for f in "${bins[@]}"; do | |
| case "$f" in *.sigstore|*.sigstore.json) bin_sigs+=("$f") ;; *) bin_files+=("$f") ;; esac | |
| done | |
| sums=(binaries/SHA256SUMS) | |
| sums_sigs=(binaries/SHA256SUMS.sigstore.json) | |
| # FAIL CLOSED — refuse to publish unless the full, signed set is present. | |
| if [ ${#dists[@]} -lt 2 ]; then | |
| echo "expected sdist + wheel; found ${#dists[@]}; aborting" >&2; exit 1 | |
| fi | |
| if [ ${#dist_sigs[@]} -lt ${#dists[@]} ]; then | |
| echo "dist signatures (${#dist_sigs[@]}) < dists (${#dists[@]}); refusing" >&2; exit 1 | |
| fi | |
| if [ ${#bin_files[@]} -ne 5 ]; then | |
| echo "expected 5 standalone binaries; found ${#bin_files[@]}; aborting" >&2; exit 1 | |
| fi | |
| if [ ${#bin_sigs[@]} -lt ${#bin_files[@]} ]; then | |
| echo "binary signatures (${#bin_sigs[@]}) < binaries (${#bin_files[@]}); refusing" >&2; exit 1 | |
| fi | |
| if [ ${#sums_sigs[@]} -eq 0 ]; then | |
| echo "SHA256SUMS not signed; refusing to publish" >&2; exit 1 | |
| fi | |
| # Stage exactly the gated set; knope uploads everything under this dir. | |
| mkdir -p dist/release-assets | |
| cp "${dists[@]}" "${dist_sigs[@]}" "${bin_files[@]}" "${bin_sigs[@]}" \ | |
| "${sums[@]}" "${sums_sigs[@]}" "${provenance}" dist/release-assets/ | |
| echo "staged $(find dist/release-assets -type f | wc -l) assets for v${VERSION}" | |
| # A SEPARATE, read-only mint for the third-party download step. The binary | |
| # fetch needs public read only, so it must not carry the app-token's | |
| # contents:write and workflows:write scopes (this job also holds | |
| # id-token:write for Sigstore). It uses the same secrets as the app-token | |
| # mint below, and create-github-app-token fails the step if they are broken. | |
| - name: Mint download-scoped GitHub App token | |
| id: download-token | |
| uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 | |
| with: | |
| client-id: ${{ secrets.CLAUSTER_CI_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAUSTER_CI_APP_PRIVATE_KEY }} | |
| permission-contents: read | |
| - name: Install knope | |
| uses: knope-dev/action@19617851f9f13ab2f27a05989c55efb18aca3675 # v2.1.2 | |
| with: | |
| # renovate: datasource=github-releases depName=knope-dev/knope extractVersion=^knope/v(?<version>.+)$ | |
| version: 0.23.2 | |
| # Download the knope binary with the read-only download-token, not the | |
| # default GITHUB_TOKEN. The shared github-actions installation rate limit | |
| # was exhausted by concurrent runs and 403'd the same release-asset fetch | |
| # in knope-prepare on 2026-09-03. The same limit can stop a publish here. | |
| github-token: ${{ steps.download-token.outputs.token }} | |
| # The write-scoped App token for knope. Minted LAST, after the third-party | |
| # download, attest, Sigstore and knope-install steps, so the contents:write + | |
| # workflows:write token exists only while knope runs. | |
| - name: Mint GitHub App token | |
| id: app-token | |
| uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 | |
| with: | |
| client-id: ${{ secrets.CLAUSTER_CI_APP_CLIENT_ID }} | |
| private-key: ${{ secrets.CLAUSTER_CI_APP_PRIVATE_KEY }} | |
| permission-contents: write | |
| # knope tags the checked-out release commit. If main moved on with a | |
| # .github/workflows change before this job ran, that commit's workflow | |
| # files differ from main. GitHub then refuses POST /releases, the call | |
| # that creates the tag (403 "Resource not accessible by integration"), | |
| # unless the token carries workflows:write. | |
| # Seen on v1.3.3, 2026-11-02. | |
| permission-workflows: write | |
| # Fail LOUD on an empty mint: knope uses this token to create the GitHub | |
| # Release and push the tag, and an App-identity tag is what lets the publish | |
| # trigger downstream automation. An empty token would degrade to GITHUB_TOKEN | |
| # (whose tag push does not re-trigger workflows) or fail confusingly. Surface | |
| # it here. (See issue #509.) | |
| - name: Verify App token was minted | |
| if: ${{ steps.app-token.outputs.token == '' }} | |
| run: | | |
| echo "::error::clauster-ci App token mint produced an empty token; \ | |
| refusing to proceed. Check the CLAUSTER_CI_APP_CLIENT_ID / \ | |
| CLAUSTER_CI_APP_PRIVATE_KEY secrets and the clauster-ci App installation." | |
| exit 1 | |
| # knope creates the draft, uploads dist/release-assets/*, and publishes. The | |
| # App token (not GITHUB_TOKEN) is what lets the published tag trigger | |
| # release.yml. If the version already matches the latest release tag (e.g. a | |
| # re-run after a successful publish), knope is a no-op for that package. | |
| - name: Create + publish the GitHub release | |
| run: knope release --verbose | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} |