0.4.0 #7
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
| --- | |
| # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json | |
| # | |
| # Attach the static musl binaries to a published GitHub Release. | |
| # | |
| # Release Please owns the release itself (version, tag, changelog body) — see | |
| # release-please.yml. This workflow only builds artifacts and uploads them, so | |
| # it must never generate release notes of its own or it would clobber the | |
| # changelog Release Please just wrote. | |
| # | |
| # The container image is built separately by docker.yml, which also triggers on | |
| # `release: published`. | |
| # | |
| # Asset naming — `domarinn_<version>_linux_<arch>`, e.g. | |
| # `domarinn_0.2.0_linux_amd64`. Three properties are deliberate: | |
| # | |
| # * The version is always the second underscore-separated field, so a | |
| # downloaded file identifies itself. Nothing else in a release does: the | |
| # binary was previously named only for its target, and a file sitting in | |
| # ~/Downloads was unidentifiable. | |
| # * `linux_amd64`, not the Rust triple `x86_64-unknown-linux-musl`. The triple | |
| # puts the literal word "unknown" (it is the *vendor* field) exactly where a | |
| # reader expects the version. `musl` is not in the name either — that these | |
| # are fully static is a documented property, not a filename. | |
| # * Underscores, matching the GoReleaser convention this project's assets are | |
| # read against, even though it does not use GoReleaser (see 007df97 for why). | |
| # | |
| # Because the names carry a version, `releases/latest/download/<name>` is no | |
| # longer constructible — GitHub has no wildcard there. Consumers resolve the tag | |
| # first via the `/releases/latest` redirect; README.md, docs/getting-started.md | |
| # and .github/actions/domarinn-eval/action.yml all do this. | |
| name: Release | |
| on: | |
| release: | |
| types: [published] | |
| permissions: | |
| contents: read | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # 1. Build the static musl binaries and stage them as workflow artifacts. | |
| # Both targets build natively — no `cross`, no QEMU — because GitHub | |
| # provides arm64 runners free to public repositories. That also means | |
| # neither leg is allowed to fail: an aarch64 binary is a shipped artifact, | |
| # not a best-effort extra. | |
| # --------------------------------------------------------------------------- | |
| binaries: | |
| name: build ${{ matrix.target }} | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # `target` is the Rust triple the compiler needs; `arch` is the name | |
| # the world uses, and the one that reaches the released filename. | |
| - target: x86_64-unknown-linux-musl | |
| arch: amd64 | |
| runner: ubuntu-24.04 | |
| - target: aarch64-unknown-linux-musl | |
| arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # mise installs the pinned Rust/Node/pnpm from .mise/config.toml + | |
| # .mise/mise.lock, the single source of toolchain versions for both CI | |
| # and releases. Caching is off: this workflow publishes artifacts, and a | |
| # poisoned cache entry must never be able to reach a shipped binary. | |
| - uses: jdx/mise-action@9e7f7633ff6f6d6048a9418a68d48f288f50eb14 # v4.2.3 | |
| with: | |
| install_args: node pnpm rust | |
| cache: false | |
| # The release binary embeds the web UI, so build web/dist first. | |
| - run: pnpm -C web install --frozen-lockfile | |
| - run: pnpm -C web build | |
| # musl-tools gives the C toolchain rusqlite's bundled SQLite (and the | |
| # rustls crypto backend) need; cmake is a hedge for crypto backends. | |
| - name: Install musl toolchain | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y --no-install-recommends musl-tools cmake | |
| # The `musl-build` task adds the rustup target itself and then builds; | |
| # same command locally and in CI. No rust-cache here (unlike ci.yml): | |
| # release artifacts build from a cold cache. | |
| - name: Build | |
| env: | |
| MUSL_TARGET: ${{ matrix.target }} | |
| run: mise run musl-build | |
| # Only the binary. Checksums are a single release-wide manifest written by | |
| # the upload job, because a per-target file cannot cover the artifacts | |
| # that do not belong to any one target. | |
| - name: Stage artifact | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| ARCH: ${{ matrix.arch }} | |
| TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| # Release Please tags bare semver (`0.2.0`, not `v0.2.0`), so this | |
| # normally strips nothing — it is here so a future tag-format change | |
| # cannot silently ship `domarinn_v0.2.0_linux_amd64`. | |
| version="${TAG#v}" | |
| mkdir -p dist | |
| name="domarinn_${version}_linux_${ARCH}" | |
| cp "target/${TARGET}/release/domarinn" "dist/${name}" | |
| file "dist/${name}" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: binary-${{ matrix.arch }} | |
| path: dist/* | |
| if-no-files-found: error | |
| # --------------------------------------------------------------------------- | |
| # 2. Catalogue the dependency graph, once for the whole release. | |
| # | |
| # This is a job of its own rather than a step in the matrix above, and the | |
| # separation is the point: the SBOM is generated from Cargo.lock and | |
| # web/pnpm-lock.yaml, which say nothing about the target triple, so the two | |
| # matrix legs used to produce the same 890-package document twice and ship | |
| # it under two names — implying a per-target dependency graph that does not | |
| # exist. One release, one SBOM. | |
| # | |
| # An empty checkout is also exactly what syft wants. On a pristine tree it | |
| # sees only the two lockfiles; after a build it would additionally crawl | |
| # target/ and node_modules/ — gigabytes of build output, for a worse | |
| # result. And `[profile.release] strip = true` (Cargo.toml) means scanning | |
| # the shipped binary would report 1 package where the lockfiles report 890. | |
| # | |
| # Both lockfiles matter: the binary embeds the built web UI, so the npm | |
| # graph is genuinely part of what ships. | |
| # --------------------------------------------------------------------------- | |
| sbom: | |
| name: catalogue dependencies | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Generate SBOM | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| path: . | |
| format: spdx-json | |
| output-file: sbom.spdx.json | |
| # This workflow owns what reaches the release; the action must not | |
| # attach assets or artifacts of its own. | |
| upload-artifact: false | |
| upload-release-assets: false | |
| # `.spdx.json`, not `.sbom.json`: the extension is how a consumer tells | |
| # SPDX from CycloneDX without parsing 1.9 MB of JSON. syft also names the | |
| # document after the directory it scanned — literally "." — so stamp the | |
| # release onto it, which is where the version belongs given the filename | |
| # deliberately carries none (see the upload job). | |
| - name: Name the document | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${TAG#v}" | |
| mkdir -p dist | |
| # -c because syft writes compact JSON and this step should change one | |
| # field, not the whole encoding. Without it jq re-indents all 900-odd | |
| # packages and the shipped asset grows ~28% (2.03 MB -> 2.60 MB in | |
| # 0.1.3) for nothing a machine reads. | |
| jq -c --arg name "domarinn-${version}" '.name = $name' sbom.spdx.json \ | |
| > "dist/domarinn_${version}.spdx.json" | |
| jq -r '"\(.name): \(.spdxVersion), \(.packages | length) packages"' \ | |
| "dist/domarinn_${version}.spdx.json" | |
| # Shipped as a release asset so it inherits the checksums and the cosign | |
| # signature. The file is also readable at a raw tag URL without any of | |
| # this — but that URL is unsigned, and a schema is something editors fetch | |
| # and trust. | |
| - name: Stage the config schema | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${TAG#v}" | |
| cp domarinn.schema.json "dist/domarinn_${version}.schema.json" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sbom | |
| path: dist/* | |
| if-no-files-found: error | |
| # --------------------------------------------------------------------------- | |
| # 3. Checksum, sign, and attach everything to the release Release Please | |
| # already published. | |
| # --------------------------------------------------------------------------- | |
| upload: | |
| name: upload release assets | |
| needs: [binaries, sbom] | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: write | |
| # Keyless cosign: the OIDC token is the signing identity, so there is no | |
| # private key to hold. Same mechanism docker/github-builder already uses | |
| # to sign the container image. | |
| id-token: write | |
| steps: | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| # One checksum manifest for the release, the convention every other Go/Rust | |
| # project ships. Generated from inside dist/ because sha256sum records the | |
| # path exactly as given: doing this from the repo root would bake in a | |
| # `dist/` prefix that does not exist for whoever downloads the release, | |
| # and their `sha256sum --check` would fail with "No such file or | |
| # directory". That shipped broken in 0.1.1. | |
| # | |
| # Operands are listed explicitly rather than globbed as `*` for two | |
| # reasons: the manifest must not end up inside itself, and `failglob` | |
| # turns a missing artifact into a failed release instead of a silently | |
| # short manifest. | |
| - name: Generate checksums | |
| env: | |
| TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| shopt -s failglob | |
| version="${TAG#v}" | |
| cd dist | |
| sha256sum -- "domarinn_${version}_linux_"* "domarinn_${version}.spdx.json" \ | |
| "domarinn_${version}.schema.json" \ | |
| > "domarinn_${version}_checksums.txt" | |
| cat "domarinn_${version}_checksums.txt" | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| # Sign every artifact — binaries, SBOM, and the checksum manifest alike — | |
| # so a consumer can verify provenance without trusting the download path. | |
| # Bundles are self-contained (certificate + signature + Rekor entry), so | |
| # verification needs no keyserver, only `cosign verify-blob --bundle`. | |
| # They are not themselves checksummed: a bundle carries its own integrity. | |
| - name: Sign artifacts | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| signed=0 | |
| for f in dist/*; do | |
| case "$f" in | |
| *.sigstore.json) continue ;; | |
| esac | |
| echo "::group::sign $f" | |
| cosign sign-blob --yes --bundle "${f}.sigstore.json" "$f" | |
| echo "::endgroup::" | |
| signed=$((signed + 1)) | |
| done | |
| # A silent zero here would publish an unsigned release that looks fine. | |
| if [ "$signed" -eq 0 ]; then | |
| echo "::error::no artifacts were signed - dist/ was empty" | |
| exit 1 | |
| fi | |
| echo "signed ${signed} artifacts" | |
| - name: Upload | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.event.release.tag_name }} | |
| GH_REPO: ${{ github.repository }} | |
| # --clobber so re-running the workflow on an existing release replaces | |
| # the assets instead of erroring out. | |
| run: gh release upload "$TAG" dist/* --clobber |