release: 0.21.15 #130
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 | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| permissions: | |
| id-token: write | |
| attestations: write | |
| contents: write | |
| jobs: | |
| sbom: | |
| name: Generate SBOM | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| # Install from crates.io rather than psastras/sbom-rs/actions/…@cargo-sbom-latest: | |
| # that tag is stale (2023, no actions/ dir) so the action ref fails to resolve. | |
| - name: Install cargo-sbom | |
| run: cargo install cargo-sbom --locked | |
| # cargo-sbom writes to stdout (no -o flag) and the SPDX value is | |
| # spdx_json_2_3 (not spdx_2_3). | |
| - name: Generate SPDX SBOM | |
| run: cargo sbom --output-format spdx_json_2_3 > sbom.spdx.json | |
| - name: Upload SBOM as release asset | |
| uses: svenstaro/upload-release-action@v2 | |
| with: | |
| repo_token: ${{ secrets.GITHUB_TOKEN }} | |
| file: sbom.spdx.json | |
| tag: ${{ github.ref }} | |
| overwrite: true | |
| - name: Upload SBOM as workflow artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sbom | |
| path: sbom.spdx.json | |
| build: | |
| name: ${{ matrix.target }} | |
| # Wait for the SBOM: the "Download SBOM" step below pulls the `sbom` | |
| # artifact, and attestation needs it. Without this the jobs race and | |
| # build can hit Download SBOM before sbom uploads (or after it fails), | |
| # dying before it uploads the binaries. | |
| needs: sbom | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| # Static musl build — portable across Linux distros (no glibc | |
| # version coupling). Works because TLS is rustls, not OpenSSL. | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-musl | |
| # Cross-compile the Intel target on the Apple-Silicon runner. | |
| # `macos-13` (Intel) runners are scarce and frequently sit | |
| # queued until the 24h timeout; `macos-latest` is plentiful and | |
| # ships both-arch SDKs. Build the no-plugin set (same as | |
| # Windows): `evil_janet`'s C lib only compiles for the host | |
| # arch, so the Janet `plugin` feature can't link for x86_64 when | |
| # cross-building. The native arm64 build below keeps the plugin. | |
| - os: macos-latest | |
| target: x86_64-apple-darwin | |
| cargo_flags: "--no-default-features --features windows-default" | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| # Windows MSVC can't build the Janet `plugin` feature | |
| # (evil_janet needs _setjmp; janetrs 0.8 passes i64 to the | |
| # Windows fseek). Build the default set minus plugin. | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| cargo_flags: "--no-default-features --features windows-default" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| target: ${{ matrix.target }} | |
| # musl needs the cross linker/toolchain. | |
| - name: Install musl tools | |
| if: matrix.target == 'x86_64-unknown-linux-musl' | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - name: Build | |
| run: cargo build --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }} | |
| # Package the `dirge` binary (named via [[bin]] regardless of the | |
| # `dirge-agent` crate name). Windows → .zip, Unix → .tar.gz; emit a | |
| # sha256 alongside so users can verify the download. | |
| - name: Package (Unix) | |
| if: matrix.os != 'windows-latest' | |
| run: | | |
| archive=dirge-${{ matrix.target }}.tar.gz | |
| tar czf "$archive" -C "target/${{ matrix.target }}/release" dirge | |
| shasum -a 256 "$archive" > "$archive.sha256" | |
| - name: Package (Windows) | |
| if: matrix.os == 'windows-latest' | |
| shell: pwsh | |
| run: | | |
| $archive = "dirge-${{ matrix.target }}.zip" | |
| Compress-Archive -Path "target/${{ matrix.target }}/release/dirge.exe" -DestinationPath $archive | |
| (Get-FileHash $archive -Algorithm SHA256).Hash | Out-File -Encoding ascii "$archive.sha256" | |
| - name: Download SBOM | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sbom | |
| - name: Attest binary (Unix) | |
| if: matrix.os != 'windows-latest' | |
| uses: actions/attest@v4 | |
| with: | |
| subject-path: dirge-${{ matrix.target }}.tar.gz | |
| sbom-path: sbom.spdx.json | |
| - name: Attest binary (Windows) | |
| if: matrix.os == 'windows-latest' | |
| uses: actions/attest@v4 | |
| with: | |
| subject-path: dirge-${{ matrix.target }}.zip | |
| sbom-path: sbom.spdx.json | |
| - name: Upload release assets | |
| uses: svenstaro/upload-release-action@v2 | |
| with: | |
| repo_token: ${{ secrets.GITHUB_TOKEN }} | |
| file: dirge-${{ matrix.target }}.* | |
| file_glob: true | |
| tag: ${{ github.ref }} | |
| overwrite: true | |
| nix-binary-package: | |
| name: Bump Nix binary package | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: main | |
| # Full history so the post-build commit can rebase+push to main. | |
| fetch-depth: 0 | |
| - name: Install Nix | |
| uses: DeterminateSystems/nix-installer-action@v22 | |
| - name: Prefetch release assets and update nix/bin.nix | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| tag="$GITHUB_REF_NAME" | |
| version="${tag#v}" | |
| assets="$(gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')" | |
| python3 - <<'PY' > /tmp/dirge-nix-assets.tsv | |
| import re | |
| from pathlib import Path | |
| text = Path("nix/bin.nix").read_text() | |
| for match in re.finditer(r'"(?P<system>[^"]+)" = \{\s*triple = "(?P<triple>[^"]+)";', text): | |
| print(f"{match.group('system')}\t{match.group('triple')}") | |
| PY | |
| hashes_json="{}" | |
| while IFS=$'\t' read -r system triple; do | |
| asset="dirge-${triple}.tar.gz" | |
| if ! grep -Fxq "$asset" <<< "$assets"; then | |
| echo "Missing release asset: $asset" >&2 | |
| exit 1 | |
| fi | |
| url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}/${asset}" | |
| hash="$(nix store prefetch-file --hash-type sha256 --json "$url" | jq -r .hash)" | |
| hashes_json="$(jq --arg system "$system" --arg hash "$hash" '. + {($system): $hash}' <<< "$hashes_json")" | |
| done < /tmp/dirge-nix-assets.tsv | |
| HASHES_JSON="$hashes_json" VERSION="$version" python3 - <<'PY' | |
| import json | |
| import os | |
| import re | |
| from pathlib import Path | |
| path = Path("nix/bin.nix") | |
| text = path.read_text() | |
| text = re.sub(r'version = "[^"]+";', f'version = "{os.environ["VERSION"]}";', text, count=1) | |
| hashes = json.loads(os.environ["HASHES_JSON"]) | |
| for system, hash_ in hashes.items(): | |
| pattern = rf'("{re.escape(system)}" = \{{\n\s+triple = "[^"]+";\n\s+hash = ")[^"]+(";)' | |
| text = re.sub(pattern, rf'\g<1>{hash_}\2', text, count=1) | |
| path.write_text(text) | |
| PY | |
| # Commit the bump straight to `main` instead of opening a PR. The | |
| # PR route (peter-evans/create-pull-request) failed on every release | |
| # with "GitHub Actions is not permitted to create or approve pull | |
| # requests" — a repo Actions toggle that's off. `main` is unprotected | |
| # and this job has contents: write (explicit permissions), so a | |
| # direct push works and needs no extra setting or PAT. A GITHUB_TOKEN | |
| # push doesn't re-trigger workflows, so this can't loop. | |
| - name: Commit nix/bin.nix bump | |
| run: | | |
| set -euo pipefail | |
| if git diff --quiet -- nix/bin.nix; then | |
| echo "nix/bin.nix already current for ${GITHUB_REF_NAME} — nothing to commit" | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add nix/bin.nix | |
| git commit -m "Bump Nix dirge-bin to ${GITHUB_REF_NAME}" | |
| # The release job can run minutes after the tag; rebase on any | |
| # main movement so the push fast-forwards. | |
| git pull --rebase origin main | |
| git push origin HEAD:main | |
| # Bump the Homebrew tap (dirge-code/homebrew-dirge) to the just-released | |
| # version + checksums. Runs after every target has uploaded its assets, so | |
| # the .sha256 files exist to read back. Needs a HOMEBREW_TAP_TOKEN secret — | |
| # a PAT (or fine-grained token) with contents:write on the tap repo, since | |
| # the default GITHUB_TOKEN can't push to another repository. If the secret | |
| # is absent the checkout step fails loudly but the binary release above is | |
| # already complete and unaffected. | |
| homebrew: | |
| name: Bump Homebrew formula | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Resolve version + checksums from the release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| tag="$GITHUB_REF_NAME" | |
| mkdir -p shas | |
| gh release download "$tag" -R "$GITHUB_REPOSITORY" -p '*.tar.gz.sha256' -D shas | |
| { | |
| echo "VERSION=${tag#v}" | |
| echo "ARM_MAC=$(awk '{print $1}' shas/dirge-aarch64-apple-darwin.tar.gz.sha256)" | |
| echo "X86_MAC=$(awk '{print $1}' shas/dirge-x86_64-apple-darwin.tar.gz.sha256)" | |
| echo "X86_LINUX=$(awk '{print $1}' shas/dirge-x86_64-unknown-linux-gnu.tar.gz.sha256)" | |
| } >> "$GITHUB_ENV" | |
| - name: Check out the tap | |
| uses: actions/checkout@v7 | |
| with: | |
| repository: dirge-code/homebrew-dirge | |
| token: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| path: tap | |
| - name: Update the formula in place | |
| run: | | |
| f=tap/Formula/dirge.rb | |
| # version line + the version segment in every release URL | |
| sed -i \ | |
| -e "s|version \"[^\"]*\"|version \"$VERSION\"|" \ | |
| -e "s|/releases/download/v[0-9][^/]*/|/releases/download/v$VERSION/|g" \ | |
| "$f" | |
| # rewrite each sha256 by the URL that immediately precedes it | |
| awk -v arm="$ARM_MAC" -v xmac="$X86_MAC" -v xlin="$X86_LINUX" ' | |
| /aarch64-apple-darwin\.tar\.gz"/ { want="arm" } | |
| /x86_64-apple-darwin\.tar\.gz"/ { want="xmac" } | |
| /x86_64-unknown-linux-gnu\.tar\.gz"/ { want="xlin" } | |
| /sha256 "/ { | |
| v = (want=="arm" ? arm : (want=="xmac" ? xmac : xlin)) | |
| sub(/sha256 "[^"]*"/, "sha256 \"" v "\"") | |
| want="" | |
| } | |
| { print } | |
| ' "$f" > "$f.tmp" && mv "$f.tmp" "$f" | |
| - name: Commit and push | |
| run: | | |
| cd tap | |
| if git diff --quiet; then | |
| echo "Formula already at $VERSION — nothing to do." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git commit -am "dirge $VERSION" | |
| git push | |
| # Publish to crates.io (dirge-agent). Was manual, and 0.20.0 never reached | |
| # the registry as a result — `cargo install dirge-agent` went 0.19.29 → | |
| # 0.21.0 (dirge-a9pc). Needs a CARGO_REGISTRY_TOKEN repo secret scoped to | |
| # publish-update on the crate. | |
| # | |
| # `needs: build` because publishing is PERMANENT — a version can be yanked | |
| # but never replaced. Gating on all five targets building means a tag that | |
| # doesn't compile everywhere doesn't become an immovable registry entry. | |
| crates-io: | |
| name: Publish to crates.io | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Install Rust | |
| uses: actions-rust-lang/setup-rust-toolchain@v1 | |
| with: | |
| toolchain: stable | |
| # A tag whose version doesn't match the manifest would publish the | |
| # WRONG version under the right tag, which no later check can undo. | |
| - name: Check the tag matches Cargo.toml | |
| run: | | |
| set -euo pipefail | |
| tag="${GITHUB_REF_NAME#v}" | |
| manifest="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')" | |
| if [ "$tag" != "$manifest" ]; then | |
| echo "::error::tag ${GITHUB_REF_NAME} disagrees with Cargo.toml version ${manifest}" >&2 | |
| exit 1 | |
| fi | |
| echo "tag and manifest agree on ${manifest}" | |
| - name: Publish | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then | |
| echo "::error::CARGO_REGISTRY_TOKEN is not set — add it as a repo secret" >&2 | |
| exit 1 | |
| fi | |
| # Idempotent: re-running a completed release must not fail. cargo | |
| # refuses to overwrite an existing version, and that refusal is the | |
| # success case here. | |
| set -o pipefail | |
| if cargo publish --locked 2>&1 | tee /tmp/publish.log; then | |
| exit 0 | |
| fi | |
| if grep -qE "already (uploaded|exists)|crate version .* is already" /tmp/publish.log; then | |
| echo "dirge-agent ${GITHUB_REF_NAME#v} is already on crates.io — nothing to do" | |
| exit 0 | |
| fi | |
| exit 1 | |
| # Bump the version shown next to the logo on https://dirge-code.github.io/. | |
| # Also manual until now, and also missed for 0.20.0 — the site still read | |
| # v0.19.29 when 0.21.0 was cut (dirge-a9pc). Same shape as the homebrew job: | |
| # needs a SITE_REPO_TOKEN secret (PAT with contents:write on the site repo) | |
| # because GITHUB_TOKEN can't push to another repository. | |
| site: | |
| name: Bump site version | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check out the site | |
| uses: actions/checkout@v7 | |
| with: | |
| repository: dirge-code/dirge-code.github.io | |
| token: ${{ secrets.SITE_REPO_TOKEN }} | |
| path: site | |
| - name: Rewrite the brand-ver span | |
| run: | | |
| set -euo pipefail | |
| version="${GITHUB_REF_NAME#v}" | |
| f=site/index.html | |
| sed -i "s|\(class=\"brand-ver\">\)v[0-9][^<]*|\1v${version}|" "$f" | |
| # An unmatched sed is a silent no-op, which is exactly the failure | |
| # mode this job exists to remove. Prove the string is there now. | |
| grep -q "class=\"brand-ver\">v${version}<" "$f" || { | |
| echo "::error::brand-ver span did not end up at v${version} — has the markup changed?" >&2 | |
| exit 1 | |
| } | |
| - name: Commit and push | |
| run: | | |
| set -euo pipefail | |
| cd site | |
| if git diff --quiet; then | |
| echo "Site already at ${GITHUB_REF_NAME} — nothing to do." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git commit -am "Bump version to ${GITHUB_REF_NAME}" | |
| git push | |
| # The actual fix for dirge-a9pc. Every job above can report success while | |
| # shipping nothing: the homebrew and site seds no-op silently if the markup | |
| # moves, and a skipped job is green by default. This reads each channel back | |
| # from its published source and fails if any of them disagrees with the tag, | |
| # so "a channel didn't ship" is a red X instead of something noticed two | |
| # releases later. | |
| verify-channels: | |
| name: Verify all release channels | |
| needs: [build, nix-binary-package, homebrew, crates-io, site] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check every channel carries this version | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -uo pipefail | |
| tag="$GITHUB_REF_NAME" | |
| version="${tag#v}" | |
| failed=0 | |
| fail() { echo "::error::$1"; failed=1; } | |
| ok() { echo "ok: $1"; } | |
| # Fetch to a file, THEN match. `gh api … | grep -q` looks equivalent | |
| # and isn't: grep -q exits on the first match, gh takes SIGPIPE, and | |
| # pipefail reports the pipeline as failed — a missing-channel error | |
| # for a channel that is fine. It only shows up on inputs long enough | |
| # to still be streaming (index.html), which is the worst way to find | |
| # out. | |
| fetch() { gh api "$1" -H "Accept: application/vnd.github.raw" > "$2"; } | |
| # 1. GitHub release — every packaged target plus its checksum. | |
| assets="$(gh release view "$tag" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name')" | |
| for want in \ | |
| dirge-x86_64-unknown-linux-gnu.tar.gz \ | |
| dirge-x86_64-unknown-linux-musl.tar.gz \ | |
| dirge-x86_64-apple-darwin.tar.gz \ | |
| dirge-aarch64-apple-darwin.tar.gz \ | |
| dirge-x86_64-pc-windows-msvc.zip \ | |
| sbom.spdx.json | |
| do | |
| if grep -Fxq "$want" <<< "$assets"; then ok "release asset $want" | |
| else fail "release $tag is missing asset $want"; fi | |
| case "$want" in | |
| sbom.spdx.json) ;; | |
| *) if grep -Fxq "$want.sha256" <<< "$assets"; then ok "release asset $want.sha256" | |
| else fail "release $tag is missing checksum $want.sha256"; fi ;; | |
| esac | |
| done | |
| # 2. crates.io. The sparse index is CDN-cached, so give it a few | |
| # minutes before calling it missing. | |
| on_registry() { | |
| curl -sf https://index.crates.io/di/rg/dirge-agent > /tmp/crates-index.json || return 1 | |
| [ "$(jq -sr --arg v "$version" 'any(.[]; .vers == $v)' /tmp/crates-index.json)" = true ] | |
| } | |
| for attempt in $(seq 1 10); do | |
| on_registry && break | |
| echo "crates.io index has no ${version} yet (attempt ${attempt}/10)" | |
| [ "$attempt" -lt 10 ] && sleep 30 | |
| done | |
| if on_registry | |
| then ok "crates.io has dirge-agent ${version}" | |
| else fail "crates.io has no dirge-agent ${version}"; fi | |
| # 3. nix/bin.nix, committed back to main by the bump job. | |
| fetch "repos/$GITHUB_REPOSITORY/contents/nix/bin.nix?ref=main" /tmp/bin.nix | |
| if grep -q "version = \"${version}\";" /tmp/bin.nix | |
| then ok "nix/bin.nix on main is at ${version}" | |
| else fail "nix/bin.nix on main is not at ${version}"; fi | |
| # 4. Homebrew tap. | |
| fetch repos/dirge-code/homebrew-dirge/contents/Formula/dirge.rb /tmp/dirge.rb | |
| if grep -q "version \"${version}\"" /tmp/dirge.rb | |
| then ok "homebrew formula is at ${version}" | |
| else fail "homebrew formula is not at ${version}"; fi | |
| # 5. Site. | |
| fetch repos/dirge-code/dirge-code.github.io/contents/index.html /tmp/index.html | |
| if grep -q "class=\"brand-ver\">v${version}<" /tmp/index.html | |
| then ok "site brand-ver is at v${version}" | |
| else fail "site brand-ver is not at v${version}"; fi | |
| if [ "$failed" -ne 0 ]; then | |
| echo "::error::one or more release channels did not receive ${tag}" | |
| exit 1 | |
| fi | |
| echo "all six channels carry ${tag}" |