From 9d08cff81e92a92b926ec84655c96b64100a66a0 Mon Sep 17 00:00:00 2001 From: Amine ElQaraoui Date: Mon, 12 Oct 2026 20:09:29 +0000 Subject: [PATCH 1/2] ci: release a prebuilt x86_64 Linux binary for every published version Every Arch program repo installs arch-kit with `cargo install`, which takes about four minutes per job; a stage deploy runs it in some twenty jobs, five of them back to back for aUSD funding alone. `Release binary` builds arch-kit from the exact commit crates.io packaged a version from (the crate's .cargo_vcs_info.json), smoke-tests `--version`, and attaches `arch-kit--x86_64-unknown-linux-gnu.tar.gz` and its SHA-256 to the GitHub release `v`, creating the release and tag at that commit. The binary needs glibc 2.39+ and OpenSSL 3 (arch_sdk's reqwest uses native-tls), i.e. ubuntu-24.04 / ubuntu-latest runners. - Publish Crate calls it after `cargo publish`; run it by hand to backfill a version published earlier (0.1.8 next). - Published assets are never replaced: an identical asset is skipped, a different one, or a tag at another commit, fails the run. - Pull requests touching the crate build and smoke-test without releasing. --- .github/workflows/publish-crate.yml | 9 ++ .github/workflows/release-binary.yml | 197 +++++++++++++++++++++++++++ README.md | 26 +++- 3 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release-binary.yml diff --git a/.github/workflows/publish-crate.yml b/.github/workflows/publish-crate.yml index 0f8530f..f84bc26 100644 --- a/.github/workflows/publish-crate.yml +++ b/.github/workflows/publish-crate.yml @@ -74,3 +74,12 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} run: cargo publish --locked + + binary: + name: Release the arch-kit ${{ inputs.version }} binary + needs: publish + permissions: + contents: write + uses: ./.github/workflows/release-binary.yml + with: + version: ${{ inputs.version }} diff --git a/.github/workflows/release-binary.yml b/.github/workflows/release-binary.yml new file mode 100644 index 0000000..b060ea4 --- /dev/null +++ b/.github/workflows/release-binary.yml @@ -0,0 +1,197 @@ +# Release binary: the arch-kit CLI, prebuilt for x86_64 Linux. +# +# Builds the binary from the exact commit a crates.io version was packaged from +# (crates.io records it in the crate's .cargo_vcs_info.json) and attaches it, +# with its SHA-256, to the GitHub release `v`, creating the release and +# its tag when missing. Workflows then download it in seconds instead of +# compiling it with `cargo install` (about four minutes on a hosted runner). +# +# The binary targets x86_64-unknown-linux-gnu, built on ubuntu-24.04: it needs +# glibc 2.39+ and OpenSSL 3 (libssl.so.3), which every ubuntu-24.04 / +# ubuntu-latest runner has. OpenSSL is linked dynamically because arch_sdk's +# reqwest uses native-tls. +# +# publish-crate.yml calls this after each publish; dispatch it by hand to add the +# binary to a version published before. A published asset is never replaced, so +# a checksum consumers pinned stays valid: a re-run that would upload different +# bytes fails instead. Pull requests build and smoke-test without releasing. + +name: Release binary + +on: + workflow_dispatch: + inputs: + version: + description: A version already published to crates.io + required: true + type: string + workflow_call: + inputs: + version: + required: true + type: string + pull_request: + paths: + - Cargo.toml + - Cargo.lock + - src/** + - templates/** + - .github/workflows/release-binary.yml + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + TARGET: x86_64-unknown-linux-gnu + VERSION: ${{ inputs.version }} + +jobs: + build: + name: Build arch-kit ${{ inputs.version || 'for this pull request' }} + runs-on: ubuntu-24.04 + outputs: + commit: ${{ steps.commit.outputs.sha }} + steps: + - name: Resolve the published commit + id: commit + if: ${{ github.event_name != 'pull_request' }} + run: | + set -euo pipefail + [[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { + echo "::error::version '${VERSION}' is not a semver version" >&2 + exit 1 + } + # Retried: right after `cargo publish` the crate takes a moment to appear. + crate="${RUNNER_TEMP}/arch-kit.crate" + fetched=false + for _ in $(seq 1 12); do + if curl -fsSL "https://static.crates.io/crates/arch-kit/arch-kit-${VERSION}.crate" -o "${crate}"; then + fetched=true + break + fi + sleep 10 + done + "${fetched}" || { echo "::error::arch-kit ${VERSION} is not on crates.io" >&2; exit 1; } + sha="$(tar -xzOf "${crate}" "arch-kit-${VERSION}/.cargo_vcs_info.json" | jq -r '.git.sha1 // empty')" + [[ "${sha}" =~ ^[0-9a-f]{40}$ ]] || { + echo "::error::arch-kit ${VERSION} was published without a recorded commit" >&2 + exit 1 + } + echo "arch-kit ${VERSION} was published from ${sha}" + echo "sha=${sha}" >> "${GITHUB_OUTPUT}" + + - name: Checkout repository + uses: actions/checkout@v5 + with: + ref: ${{ steps.commit.outputs.sha || github.sha }} + + - name: Check the version + if: ${{ github.event_name != 'pull_request' }} + run: | + set -euo pipefail + crate_version="$( + cargo metadata --no-deps --format-version 1 | + jq -r '.packages[] | select(.name == "arch-kit") | .version' + )" + test "${crate_version}" = "${VERSION}" || { + echo "::error::the published commit's Cargo.toml says ${crate_version}, not ${VERSION}" >&2 + exit 1 + } + + - name: Set up Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" + + - name: Build + run: cargo build --locked --release --bin arch-kit + + - name: Smoke test + run: | + set -euo pipefail + reported="$(target/release/arch-kit --version)" + echo "${reported}" + if [ -n "${VERSION}" ]; then + test "${reported}" = "arch-kit ${VERSION}" + fi + + - name: Package + run: | + set -euo pipefail + name="arch-kit-${VERSION:-pr}-${TARGET}" + mkdir -p dist + tar -czf "dist/${name}.tar.gz" -C target/release arch-kit + (cd dist && sha256sum "${name}.tar.gz" > "${name}.tar.gz.sha256") + cat "dist/${name}.tar.gz.sha256" + + - name: Upload the package + uses: actions/upload-artifact@v4 + with: + name: arch-kit-${{ env.TARGET }} + path: dist/ + if-no-files-found: error + + release: + name: Attach to release v${{ inputs.version }} + needs: build + if: ${{ github.event_name != 'pull_request' }} + runs-on: ubuntu-24.04 + permissions: + contents: write + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + COMMIT: ${{ needs.build.outputs.commit }} + steps: + - name: Download the package + uses: actions/download-artifact@v4 + with: + name: arch-kit-${{ env.TARGET }} + path: dist + + - name: Create or update the release + run: | + set -euo pipefail + tag="v${VERSION}" + name="arch-kit-${VERSION}-${TARGET}" + sha256="$(cut -d' ' -f1 "dist/${name}.tar.gz.sha256")" + if gh release view "${tag}" -R "${REPO}" > /dev/null 2>&1; then + tag_commit="$(gh api "repos/${REPO}/commits/${tag}" --jq .sha)" + test "${tag_commit}" = "${COMMIT}" || { + echo "::error::tag ${tag} points at ${tag_commit}, but ${VERSION} was published from ${COMMIT}" >&2 + exit 1 + } + else + notes="$(printf '%s\n' \ + "arch-kit ${VERSION}, built from ${COMMIT} (the commit crates.io packaged it from)." \ + "" \ + "\`${name}.tar.gz\` (x86_64 Linux, glibc 2.39+ and OpenSSL 3, e.g. ubuntu-24.04):" \ + "" \ + '```' \ + "SHA-256 ${sha256}" \ + '```')" + gh release create "${tag}" -R "${REPO}" --target "${COMMIT}" \ + --title "arch-kit ${VERSION}" --notes "${notes}" + fi + # Published bytes never change: skip an identical asset, refuse a different one. + for file in "${name}.tar.gz" "${name}.tar.gz.sha256"; do + existing="${RUNNER_TEMP}/existing" + rm -rf "${existing}" + if gh release download "${tag}" -R "${REPO}" -p "${file}" -D "${existing}" 2> /dev/null; then + cmp -s "${existing}/${file}" "dist/${file}" || { + echo "::error::release ${tag} already has a different ${file}; published assets are never replaced" >&2 + exit 1 + } + echo "${file}: already attached" + else + gh release upload "${tag}" -R "${REPO}" "dist/${file}" + echo "${file}: attached" + fi + done + { + echo "### arch-kit ${VERSION} binary" + echo "- release: https://github.com/${REPO}/releases/tag/${tag}" + echo "- commit: \`${COMMIT}\`" + echo "- \`${name}.tar.gz\` SHA-256: \`${sha256}\`" + } >> "${GITHUB_STEP_SUMMARY}" diff --git a/README.md b/README.md index bc5be15..f2055f2 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,24 @@ After pulling changes, update the installed binary with: cargo install --path . --force ``` +### Prebuilt binary (x86_64 Linux) + +Every published version also has a prebuilt binary on its GitHub release +`v`, built from the commit crates.io packaged it from. It needs glibc +2.39+ and OpenSSL 3, as on Ubuntu 24.04 and the `ubuntu-24.04` / +`ubuntu-latest` GitHub runners. Downloading it takes seconds where +`cargo install` compiles for minutes, so CI should prefer it; pin the SHA-256 +from the release notes: + +```bash +ARCH_KIT_VERSION=0.1.8 +ARCH_KIT_SHA256= +name="arch-kit-${ARCH_KIT_VERSION}-x86_64-unknown-linux-gnu.tar.gz" +curl -fsSLO "https://github.com/Arch-Network/arch-kit/releases/download/v${ARCH_KIT_VERSION}/${name}" +echo "${ARCH_KIT_SHA256} ${name}" | sha256sum -c - +mkdir -p "$HOME/.local/bin" && tar -xzf "${name}" -C "$HOME/.local/bin" arch-kit +``` + ## GitHub deployment workflow Store the program and authority key contents as `PROGRAM_KEY` and @@ -442,5 +460,9 @@ Maintainers publish from GitHub Actions using the `Publish Crate` workflow. Configure a protected `crates-io` environment containing the `CRATES_IO_TOKEN` secret, update `Cargo.toml`, `Cargo.lock`, and the pinned `ARCH_KIT_VERSION` in the reusable workflow, then run the workflow with that -version. After publication, create the matching immutable Git tag, such as -`v0.1.0`, for workflow consumers. +version. After publication it calls `Release binary`, which builds the x86_64 +Linux binary from the commit crates.io packaged and attaches it, with its +SHA-256, to the GitHub release `v`, creating that release and its +immutable tag. To add a binary to a version published earlier, run +`Release binary` by hand with the version. Published assets are never replaced; +if only the release job failed, re-run that job rather than the whole workflow. From 91b002b84db681d0d0f2c8ecb610a0323ec248a9 Mon Sep 17 00:00:00 2001 From: Amine ElQaraoui Date: Mon, 12 Oct 2026 20:31:05 +0000 Subject: [PATCH 2/2] release-binary: check an existing tag even without a release; pin the checksum in old notes Review follow-ups: - A tag v that exists without a release skipped the commit check, and `gh release create --target` does not move an existing tag, so a binary could land on a tag at another commit. The tag is now checked whenever it exists (git/matching-refs answers [] for an absent tag, so a failed lookup cannot pass for "no tag"), before anything is created. - Backfilling into a release that already existed left its notes without the SHA-256 the README tells consumers to pin. Its notes now gain the checksum section once, keeping their content. --- .github/workflows/release-binary.yml | 35 +++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-binary.yml b/.github/workflows/release-binary.yml index b060ea4..5515dff 100644 --- a/.github/workflows/release-binary.yml +++ b/.github/workflows/release-binary.yml @@ -156,21 +156,27 @@ jobs: tag="v${VERSION}" name="arch-kit-${VERSION}-${TARGET}" sha256="$(cut -d' ' -f1 "dist/${name}.tar.gz.sha256")" - if gh release view "${tag}" -R "${REPO}" > /dev/null 2>&1; then - tag_commit="$(gh api "repos/${REPO}/commits/${tag}" --jq .sha)" + # An existing tag must already point at the published commit: `--target` + # only places a tag that does not exist yet. matching-refs answers [] for + # an absent tag, so a failed lookup cannot pass for "no tag". + tag_refs="$(gh api "repos/${REPO}/git/matching-refs/tags/${tag}" \ + --jq "[.[] | select(.ref == \"refs/tags/${tag}\")] | length")" + if [ "${tag_refs}" != 0 ]; then + tag_commit="$(gh api "repos/${REPO}/commits/refs/tags/${tag}" --jq .sha)" test "${tag_commit}" = "${COMMIT}" || { echo "::error::tag ${tag} points at ${tag_commit}, but ${VERSION} was published from ${COMMIT}" >&2 exit 1 } - else - notes="$(printf '%s\n' \ - "arch-kit ${VERSION}, built from ${COMMIT} (the commit crates.io packaged it from)." \ - "" \ - "\`${name}.tar.gz\` (x86_64 Linux, glibc 2.39+ and OpenSSL 3, e.g. ubuntu-24.04):" \ - "" \ - '```' \ - "SHA-256 ${sha256}" \ - '```')" + fi + notes="$(printf '%s\n' \ + "arch-kit ${VERSION}, built from ${COMMIT} (the commit crates.io packaged it from)." \ + "" \ + "\`${name}.tar.gz\` (x86_64 Linux, glibc 2.39+ and OpenSSL 3, e.g. ubuntu-24.04):" \ + "" \ + '```' \ + "SHA-256 ${sha256}" \ + '```')" + if ! gh release view "${tag}" -R "${REPO}" > /dev/null 2>&1; then gh release create "${tag}" -R "${REPO}" --target "${COMMIT}" \ --title "arch-kit ${VERSION}" --notes "${notes}" fi @@ -189,6 +195,13 @@ jobs: echo "${file}: attached" fi done + # A release that existed before (a backfill) keeps its notes and gains the + # checksum consumers pin, once. + body="$(gh release view "${tag}" -R "${REPO}" --json body --jq .body)" + if ! grep -qF "SHA-256 ${sha256}" <<< "${body}"; then + gh release edit "${tag}" -R "${REPO}" --notes "$(printf '%s\n\n%s\n' "${body}" "${notes}")" + echo "release notes: checksum added" + fi { echo "### arch-kit ${VERSION} binary" echo "- release: https://github.com/${REPO}/releases/tag/${tag}"