Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/publish-crate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
210 changes: 210 additions & 0 deletions .github/workflows/release-binary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# 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<version>`, 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")"
# 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
}
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}"
Comment on lines +180 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Validate an existing tag before creating its release

When v${VERSION} already exists without a GitHub Release, as can happen during a historical backfill, this branch skips the tag-commit check and relies on --target. However, gh release create --help states that --target controls the automatic tag creation when the matching tag does not yet exist, so an existing tag is not retargeted. The workflow can therefore attach a binary built from COMMIT to a release whose tag points to a different commit; resolve and validate the tag independently of whether the release exists.

Useful? React with 👍 / 👎.

@0xfinetuned 0xfinetuned Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91b002b: the tag is now checked whenever it exists, before anything is created. Existence comes from git/matching-refs with an exact-name filter, which answers [] when the tag is absent, so a failed lookup can't pass for "no tag". A tag at another commit fails the run.

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
# 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}"
echo "- commit: \`${COMMIT}\`"
echo "- \`${name}.tar.gz\` SHA-256: \`${sha256}\`"
} >> "${GITHUB_STEP_SUMMARY}"
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>`, 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=<SHA-256 from the v0.1.8 release notes>
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
Expand Down Expand Up @@ -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<version>`, 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.
Loading