From b62be02dcb9078dfd904f6c343d7c41e1a937a84 Mon Sep 17 00:00:00 2001 From: m-wells Date: Wed, 24 Jun 2026 17:03:18 -0400 Subject: [PATCH 1/2] ci: migrate to GitHub-hosted runners + attest release provenance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move CI/docs/release off the self-hosted fleet (`homeserver-pool` ARC pool + the `ghost` M2 Max) onto GitHub-hosted runners, and add build-provenance attestation to release assets. Runner migration: - Linux jobs: `homeserver-pool` -> `ubuntu-latest`, keeping the `rust-ci`/`rust-web-ci` containers for toolchain + prettier parity. - macOS jobs: `[self-hosted, macos, arm64]` -> `macos-14` (hosted Apple Silicon). - Drop the `ghost`-specific rustup-shim PATH hacks (`rustup which cargo` -> $GITHUB_PATH) in ci.yml and release.yml — hosted macOS has rustup + cargo on PATH already. - ci.yml macOS: add `npm install -g prettier@3.8.4` before `make check`, since `make check` -> lint runs `prettier --check .` as a global binary and the hosted macOS image has no global prettier. Pinned to the version baked into the rust-web-ci container to keep macOS in lockstep with Linux. - Refresh comments the runner swap made inaccurate. Release attestation: - Add `attestations: write` to release.yml's top-level permissions (reusing the existing `id-token: write`). - Add `actions/attest-build-provenance@v4` in the release job, after download-artifact and before action-gh-release, attesting `dist/themis-*.tar.gz` (the glob skips the `.tar.gz.sha256` sidecars). Out of scope (unchanged): crates.io publishing/OIDC, version-verify logic, artifact naming (downstream pkgbuilds/homebrew parse these), the Makefile, and the docs.yml private/public trigger comment block. Claude-Session: https://claude.ai/code/session_01U5qTPzFxSPun9VccwbLcCy --- .github/workflows/ci.yml | 64 +++++++++++++++-------------------- .github/workflows/docs.yml | 17 +++++----- .github/workflows/release.yml | 45 +++++++++++++----------- 3 files changed, 61 insertions(+), 65 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8965112..e6f6db7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,20 +1,13 @@ # CI — lint, build, and test Themis on every push. # -# Runs on the self-hosted ARC pool `homeserver-pool` (ephemeral Linux pods, -# minRunners: 0 — a fresh container is spun up per job and torn down after). +# Runs on GitHub-hosted runners (Linux in the `rust-ci`-family container for +# toolchain parity; macOS natively on a hosted Apple Silicon image). # # There is intentionally NO `pull_request` trigger. Fork PRs fire only # pull_request events (their push happened in the fork), so with -# push/workflow_dispatch-only triggers a fork PR can never schedule a job onto -# the self-hosted runners. If you later want CI for outside contributors' PRs, -# add a SEPARATE workflow with `on: pull_request` pinned to -# `runs-on: ubuntu-latest` (GitHub-hosted, free on public repos, fork-safe). -# -# Runner provisioning required on `homeserver-pool` (bake into the ARC image or -# install via ansible): -# - rustup (the pinned 1.95 toolchain + rustfmt/clippy install -# themselves from rust-toolchain.toml on first cargo call) -# - cc/gcc, git, make, pkg-config (make check shells out to all of these) +# push/workflow_dispatch-only triggers a fork PR can never schedule a job. If +# you later want CI for outside contributors' PRs, add a SEPARATE workflow with +# `on: pull_request` (already fork-safe here — the jobs run on hosted runners). name: CI on: @@ -28,10 +21,10 @@ concurrency: jobs: check: - runs-on: homeserver-pool - # The ARC pod is a bare runner. This image (rust-web-ci = rust-ci + Node + - # prettier) bakes in rustup + the pinned toolchain + cc/make/git + prettier, - # so `make check` (which runs `prettier --check`) needs no per-job installs. + runs-on: ubuntu-latest + # Run inside our toolchain image (rust-web-ci = rust-ci + Node + prettier): + # it bakes in rustup + the pinned toolchain + cc/make/git + prettier, so + # `make check` (which runs `prettier --check`) needs no per-job installs. # The Rust-only siblings use the leaner `rust-ci`. container: ghcr.io/twowells/rust-web-ci:latest # Inside a container job GitHub's default shell is sh (dash); force bash so @@ -65,38 +58,37 @@ jobs: - name: make check run: make check - # macOS check — native on the self-hosted M2 Max (`ghost`: labels - # macos/arm64/self-hosted). No container: macOS runners can't use Linux - # container images, so the host carries the tools — rustup + the pinned - # toolchain (honored from rust-toolchain.toml), clang/make/git (Xcode CLT), - # and prettier 3.8.4. The cargo tools install per-job below, mirroring the - # Linux job. + # macOS check — native on a GitHub-hosted Apple Silicon runner (macos-14). + # No container: macOS runners can't use Linux container images, so the host + # carries the tools — rustup + cargo are already on PATH, clang/make/git ship + # in the image's Xcode CLT, and Node/npm are preinstalled (we install prettier + # below to match the Linux container). The cargo tools install per-job, + # mirroring the Linux job. check-macos: - runs-on: [self-hosted, macos, arm64] + runs-on: macos-14 defaults: run: shell: bash steps: - uses: actions/checkout@v7 - # `ghost` exposes the toolchain via rustup shims in ~/.cargo/bin, which - # proved unreliable here: `rustc` exited 127 (shell couldn't resolve it) - # while `rustup` itself worked. Derive the active toolchain's real bin dir - # from rustup and put it on PATH — independent of the shim layer. `rustup - # show` also materializes the pinned toolchain (rust-toolchain.toml) if the - # runner hasn't cached it yet. - - name: Put Rust toolchain on PATH - run: | - rustup show - cargo_bin="$(dirname "$(rustup which cargo)")" - echo "$cargo_bin" >> "$GITHUB_PATH" + # rustup reads rust-toolchain.toml (channel 1.95 + rustfmt/clippy) and + # materializes the pinned toolchain if the runner hasn't cached it yet. + - name: Materialize pinned Rust toolchain + run: rustup show - name: Verify toolchain run: rustc --version && cargo --version - uses: Swatinem/rust-cache@v2 - # Prebuilt binaries — the host runner is persistent, but installing here - # keeps the tool versions in lockstep with the Linux job. + # The hosted macOS image ships Node/npm but no global prettier, and `make + # check` → `lint` invokes `prettier` as a global binary. Install the same + # version the rust-web-ci container pins so macOS stays in lockstep with + # the Linux job. + - name: Install prettier (match the rust-web-ci container's pinned version) + run: npm install -g prettier@3.8.4 + + # Prebuilt binaries — keeps the tool versions in lockstep with the Linux job. - name: Install cargo tools uses: taiki-e/install-action@v2 with: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f0f3ebc..79df9fb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,12 +2,12 @@ # # Two jobs: # build builds `website/dist/` and uploads it as a Pages artifact. Runs on -# the self-hosted ARC pool `homeserver-pool` using the `rust-web-ci` -# image (it bakes in Node + npm). This job never touches the Pages -# deployment, so it cannot fail the deploy even while Pages is off. +# a GitHub-hosted runner inside the `rust-web-ci` image (it bakes in +# Node + npm). This job never touches the Pages deployment, so it +# cannot fail the deploy even while Pages is off. # deploy publishes the uploaded artifact with the official `actions/deploy-pages` -# flow. This needs a GitHub-hosted runner and the `github-pages` -# environment, neither of which exists on the self-hosted pool. +# flow. This needs the `github-pages` environment (set up under +# Settings → Environments). # # --- TRIGGER: kept manual while the repo is PRIVATE ------------------------- # The repo is private and GitHub Pages is not enabled yet, so an automatic @@ -39,7 +39,7 @@ concurrency: jobs: build: - runs-on: homeserver-pool + runs-on: ubuntu-latest # rust-web-ci = rust-ci + Node + prettier. We only need Node + npm here. container: ghcr.io/twowells/rust-web-ci:latest # Inside a container job GitHub's default shell is sh (dash); force bash so @@ -78,9 +78,8 @@ jobs: deploy: needs: build - # GitHub-hosted runner: actions/deploy-pages targets the Pages service and - # needs the `github-pages` environment, which the self-hosted pool can't - # provide. Free for public repos. + # actions/deploy-pages targets the Pages service and needs the + # `github-pages` environment (declared below). Free for public repos. runs-on: ubuntu-latest environment: name: github-pages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c152bf..941fc63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,8 @@ # Release — on a vX.Y.Z tag push: # 1. verify the tag matches Cargo.toml's version -# 2. build the release binaries on the self-hosted fleet -# (Linux x86_64 on the ARC pool + macOS arm64 on ghost) +# 2. build the release binaries on GitHub-hosted runners +# (Linux x86_64 in a container + macOS arm64 on macos-14), +# each attested with build provenance at release time # 3. publish-crate publish `themis-cli` to crates.io via Trusted Publishing # (OIDC — no stored token); the installed binary stays `themis` # 4. release cut a GitHub Release with the binary + SHA256 checksum @@ -36,12 +37,13 @@ on: permissions: contents: write # create the GitHub Release - id-token: write # OIDC token for crates.io Trusted Publishing + id-token: write # OIDC token for crates.io Trusted Publishing + attestation + attestations: write # mint build-provenance attestations for the release assets jobs: # Guard: a pushed tag whose version disagrees with Cargo.toml is a mistake. verify: - runs-on: homeserver-pool + runs-on: ubuntu-latest outputs: version: ${{ steps.v.outputs.version }} steps: @@ -59,8 +61,8 @@ jobs: echo "version=$tag" >> "$GITHUB_OUTPUT" # Native release build per platform. fail-fast: false so one platform's - # failure does not cancel the others. Linux x86_64 on the ARC pool + - # aarch64-apple-darwin natively on the self-hosted ghost runner. + # failure does not cancel the others. Linux x86_64 in a container on a hosted + # runner + aarch64-apple-darwin natively on a hosted Apple Silicon runner. build: needs: verify strategy: @@ -68,13 +70,14 @@ jobs: matrix: include: - target: x86_64-unknown-linux-gnu - runner: homeserver-pool + runner: ubuntu-latest container: ghcr.io/twowells/rust-ci:latest - # Native build on the self-hosted M2 Max (`ghost`). container "" → the - # job runs on the host, not in a container (macOS can't use Linux - # images). `rustup target add` and both apple targets are already present. + # Native build on a GitHub-hosted Apple Silicon runner. container "" → + # the job runs on the host, not in a container (macOS can't use Linux + # images). rustup + cargo are already on PATH; `rustup target add` + # below ensures the apple target is installed. - target: aarch64-apple-darwin - runner: [self-hosted, macos, arm64] + runner: macos-14 container: "" runs-on: ${{ matrix.runner }} container: ${{ matrix.container }} @@ -83,13 +86,9 @@ jobs: shell: bash steps: - uses: actions/checkout@v7 - # `ghost`'s rustup shims (~/.cargo/bin) proved unreliable (rustc unresolved - # while rustup worked); derive the toolchain's real bin dir from rustup and - # put it on PATH. No-op on Linux (the container has cargo on PATH), so gate - # it to macOS. - - name: Put Rust toolchain on PATH (macOS native runner) - if: runner.os == 'macOS' - run: echo "$(dirname "$(rustup which cargo)")" >> "$GITHUB_PATH" + # rustup reads rust-toolchain.toml; `rustup target add` ensures the matrix + # target is installed. Works as-is on both the Linux container (cargo on + # PATH) and the hosted macOS runner (rustup + cargo preinstalled on PATH). - name: Materialize Rust toolchain run: | rustup show @@ -120,7 +119,7 @@ jobs: # re-run (or a re-pushed tag) is a harmless no-op rather than a hard failure. publish-crate: needs: verify - runs-on: homeserver-pool + runs-on: ubuntu-latest container: ghcr.io/twowells/rust-ci:latest defaults: run: @@ -156,12 +155,18 @@ jobs: release: needs: [verify, build, publish-crate] - runs-on: homeserver-pool + runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v8 with: path: dist merge-multiple: true + # Mint a build-provenance attestation for the platform tarballs (the glob + # deliberately skips the .tar.gz.sha256 sidecars — no value attesting + # checksum files). Consumers can verify with `gh attestation verify`. + - uses: actions/attest-build-provenance@v4 + with: + subject-path: dist/themis-*.tar.gz - uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} From 465a0a2b3d42717e981a097da0ad183eefa6d4fb Mon Sep 17 00:00:00 2001 From: m-wells Date: Wed, 24 Jun 2026 17:09:29 -0400 Subject: [PATCH 2/2] docs: reconcile docs.yml trigger comment with public/Pages-enabled reality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header claimed the repo was private, Pages disabled, and the workflow manual-only — but the push-to-main auto-deploy is live and Pages is enabled (Actions source, deploying from main). Update the comment to match; no behavior change. Claude-Session: https://claude.ai/code/session_01U5qTPzFxSPun9VccwbLcCy --- .github/workflows/docs.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 79df9fb..18e85a2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,20 +3,17 @@ # Two jobs: # build builds `website/dist/` and uploads it as a Pages artifact. Runs on # a GitHub-hosted runner inside the `rust-web-ci` image (it bakes in -# Node + npm). This job never touches the Pages deployment, so it -# cannot fail the deploy even while Pages is off. +# Node + npm). This job never touches the Pages deployment, so a +# build failure here can't half-apply a deploy. # deploy publishes the uploaded artifact with the official `actions/deploy-pages` # flow. This needs the `github-pages` environment (set up under # Settings → Environments). # -# --- TRIGGER: kept manual while the repo is PRIVATE ------------------------- -# The repo is private and GitHub Pages is not enabled yet, so an automatic -# push-to-main deploy would run-and-fail and turn `main` red. Until the repo is -# flipped public AND Pages is enabled (Settings → Pages → Source: GitHub -# Actions), this workflow runs ONLY via manual `workflow_dispatch`. -# -# TO ACTIVATE AT THE PUBLIC FLIP: uncomment the single `push` block below. -# (One-line change — that's all that's needed once Pages is on.) +# --- TRIGGER ---------------------------------------------------------------- +# The repo is public and GitHub Pages is enabled (Settings → Pages → Source: +# GitHub Actions, deploying from `main`), so this auto-deploys on every push to +# `main`; `workflow_dispatch` is also kept for manual re-runs. The published +# site is live at https://twowells.github.io/Themis/. name: Docs on: