Skip to content

Commit faa97e6

Browse files
authored
Merge pull request #153 from A3S-Lab/fix/release-distribution-recovery-20260723
fix(release): make registry publishing reproducible
2 parents f0129a7 + f322b96 commit faa97e6

3 files changed

Lines changed: 213 additions & 31 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Publish runtime dependency
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: Exact a3s-runtime version without a v prefix
8+
required: true
9+
type: string
10+
release_ref:
11+
description: Exact 40-character a3s-runtime commit SHA
12+
required: true
13+
type: string
14+
15+
permissions:
16+
contents: read
17+
18+
concurrency:
19+
group: publish-a3s-runtime-${{ inputs.version }}
20+
cancel-in-progress: false
21+
22+
env:
23+
CARGO_TERM_COLOR: always
24+
RELEASE_REF: ${{ inputs.release_ref }}
25+
RELEASE_VERSION: ${{ inputs.version }}
26+
27+
jobs:
28+
publish:
29+
name: Publish pinned a3s-runtime
30+
runs-on: ubuntu-24.04
31+
timeout-minutes: 20
32+
steps:
33+
- name: Validate inputs
34+
run: |
35+
set -euo pipefail
36+
if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
37+
echo "::error::Expected a stable SemVer version, got '$RELEASE_VERSION'"
38+
exit 1
39+
fi
40+
if [[ ! "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]]; then
41+
echo "::error::Expected an exact 40-character commit SHA"
42+
exit 1
43+
fi
44+
45+
- name: Check out A3S Box
46+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
47+
with:
48+
path: box
49+
50+
- name: Check out exact a3s-runtime commit
51+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
52+
with:
53+
repository: A3S-Lab/Runtime
54+
ref: ${{ env.RELEASE_REF }}
55+
path: runtime
56+
57+
- name: Install Rust 1.85
58+
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4
59+
with:
60+
toolchain: "1.85.0"
61+
components: rustfmt
62+
63+
- name: Verify pinned release source
64+
shell: bash
65+
run: |
66+
set -euo pipefail
67+
mapfile -t PINNED < <(
68+
python3 - <<'PY'
69+
import tomllib
70+
71+
with open("box/src/Cargo.toml", "rb") as manifest:
72+
dependency = tomllib.load(manifest)["workspace"]["dependencies"]["a3s-runtime"]
73+
print(dependency["version"])
74+
print(dependency["rev"])
75+
PY
76+
)
77+
if [ "${PINNED[0]:-}" != "$RELEASE_VERSION" ] ||
78+
[ "${PINNED[1]:-}" != "$RELEASE_REF" ]; then
79+
echo "::error::Requested release does not match the A3S Box dependency pin"
80+
exit 1
81+
fi
82+
if [ "$(git -C runtime rev-parse HEAD)" != "$RELEASE_REF" ]; then
83+
echo "::error::Runtime checkout does not match the requested release commit"
84+
exit 1
85+
fi
86+
PACKAGE_VERSION="$(
87+
cargo metadata --locked --no-deps --format-version 1 \
88+
--manifest-path runtime/Cargo.toml |
89+
jq -r '.packages[] | select(.name == "a3s-runtime") | .version'
90+
)"
91+
if [ "$PACKAGE_VERSION" != "$RELEASE_VERSION" ]; then
92+
echo "::error::Cargo version $PACKAGE_VERSION does not match $RELEASE_VERSION"
93+
exit 1
94+
fi
95+
cargo fmt --all --check --manifest-path runtime/Cargo.toml
96+
cargo test --locked --all-targets --manifest-path runtime/Cargo.toml
97+
cargo publish --locked --dry-run --manifest-path runtime/Cargo.toml
98+
99+
- name: Publish and verify crate
100+
env:
101+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
102+
run: |
103+
set -euo pipefail
104+
CRATE_URL="https://crates.io/api/v1/crates/a3s-runtime/$RELEASE_VERSION"
105+
USER_AGENT="A3S-Lab-Box-runtime-publisher/$RELEASE_VERSION (https://github.com/A3S-Lab/Box)"
106+
107+
crate_status() {
108+
curl --retry 5 --retry-all-errors --silent --show-error \
109+
--location --user-agent "$USER_AGENT" \
110+
--output /dev/null --write-out '%{http_code}' "$CRATE_URL"
111+
}
112+
113+
STATUS="$(crate_status)"
114+
case "$STATUS" in
115+
200)
116+
echo "a3s-runtime@$RELEASE_VERSION already exists; skipping upload"
117+
;;
118+
404)
119+
cargo publish --locked --manifest-path runtime/Cargo.toml
120+
;;
121+
*)
122+
echo "::error::crates.io returned HTTP $STATUS before publication"
123+
exit 1
124+
;;
125+
esac
126+
127+
for attempt in $(seq 1 18); do
128+
STATUS="$(crate_status)"
129+
if [ "$STATUS" = "200" ]; then
130+
echo "Verified a3s-runtime@$RELEASE_VERSION on crates.io"
131+
exit 0
132+
fi
133+
if [ "$STATUS" != "404" ]; then
134+
echo "::error::crates.io returned HTTP $STATUS during verification"
135+
exit 1
136+
fi
137+
if [ "$attempt" -lt 18 ]; then
138+
sleep 10
139+
fi
140+
done
141+
echo "::error::a3s-runtime@$RELEASE_VERSION did not become visible on crates.io"
142+
exit 1

.github/workflows/release.yml

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -715,45 +715,85 @@ jobs:
715715
- name: Publish crates
716716
env:
717717
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
718-
# NOTE: crates.io is NOT the primary distribution channel — the GitHub
719-
# Release tarballs + Homebrew are. This step is best-effort but
720-
# MUST NOT silently report success on a real failure (it used to:
721-
# `cargo publish || echo "Skipped"` masked 403s / version-requirement
722-
# errors as "may already be published"). Now an already-published
723-
# version is skipped truthfully and a genuine failure is a ::warning::.
718+
# The GitHub Release tarballs + Homebrew are the primary distribution
719+
# channel, but crates.io publication remains strict and idempotent.
724720
run: |
721+
set -euo pipefail
725722
cd src
726723
VERSION="${GITHUB_REF_NAME#v}"
727724
echo "Publishing version $VERSION (in dependency order)"
728-
FAILED=""
729-
libkrun_code=$(curl -s -o /dev/null -w '%{http_code}' \
730-
"https://crates.io/api/v1/crates/a3s-libkrun-sys/$VERSION")
731-
if [ "$libkrun_code" != "200" ]; then
732-
echo "::error title=missing a3s-libkrun-sys release::Publish and verify a3s-libkrun-sys@$VERSION with publish-libkrun-sys.yml before running the general release."
733-
exit 1
734-
fi
725+
CRATES_IO_USER_AGENT="A3S-Lab-Box-release-publisher/$VERSION (https://github.com/A3S-Lab/Box)"
726+
727+
crate_status() {
728+
local crate="$1"
729+
curl --retry 5 --retry-all-errors --silent --show-error \
730+
--location --user-agent "$CRATES_IO_USER_AGENT" \
731+
--output /dev/null --write-out '%{http_code}' \
732+
"https://crates.io/api/v1/crates/$crate/$VERSION"
733+
}
734+
735+
wait_for_crate() {
736+
local crate="$1"
737+
local attempt status
738+
for attempt in $(seq 1 18); do
739+
status="$(crate_status "$crate")"
740+
if [ "$status" = "200" ]; then
741+
echo "Verified $crate@$VERSION on crates.io"
742+
return 0
743+
fi
744+
if [ "$status" != "404" ]; then
745+
echo "::error::crates.io returned HTTP $status while verifying $crate@$VERSION"
746+
return 1
747+
fi
748+
if [ "$attempt" -lt 18 ]; then
749+
sleep 10
750+
fi
751+
done
752+
echo "::error::$crate@$VERSION did not become visible on crates.io"
753+
return 1
754+
}
755+
756+
libkrun_code="$(crate_status a3s-libkrun-sys)"
757+
case "$libkrun_code" in
758+
200) ;;
759+
404)
760+
echo "::error title=missing a3s-libkrun-sys release::Publish and verify a3s-libkrun-sys@$VERSION with publish-libkrun-sys.yml before running the general release."
761+
exit 1
762+
;;
763+
*)
764+
echo "::error::crates.io returned HTTP $libkrun_code while checking a3s-libkrun-sys@$VERSION"
765+
exit 1
766+
;;
767+
esac
768+
735769
# Order matters: a dependent is published AFTER its internal deps so
736770
# the index has them. libkrun-sys is deliberately excluded: its
737771
# dedicated workflow performs native clean-package, size, and
738772
# corresponding-source gates that this stub-mode job cannot bypass.
739773
for crate in a3s-box-core a3s-box-netproxy a3s-box-runtime a3s-box-sdk; do
740-
code=$(curl -s -o /dev/null -w '%{http_code}' "https://crates.io/api/v1/crates/$crate/$VERSION")
741-
if [ "$code" = "200" ]; then
742-
echo "✓ $crate@$VERSION already on crates.io — skipping"
743-
continue
744-
fi
745-
echo "Publishing $crate@$VERSION..."
746-
if cargo publish --locked -p "$crate" --allow-dirty; then
747-
echo "✓ published $crate@$VERSION"
748-
sleep 30 # let the sparse index update before dependents publish
749-
else
750-
echo "::warning title=crates.io publish failed::$crate@$VERSION failed (token/auth or version requirement). GitHub Release + Homebrew are unaffected."
751-
FAILED="$FAILED $crate"
752-
fi
774+
code="$(crate_status "$crate")"
775+
case "$code" in
776+
200)
777+
echo "$crate@$VERSION already exists; skipping upload"
778+
;;
779+
404)
780+
echo "Publishing $crate@$VERSION..."
781+
if ! cargo publish --locked -p "$crate" --allow-dirty; then
782+
code="$(crate_status "$crate")"
783+
if [ "$code" != "200" ]; then
784+
echo "::error::$crate@$VERSION publication failed"
785+
exit 1
786+
fi
787+
echo "$crate@$VERSION became visible after a concurrent publication"
788+
fi
789+
;;
790+
*)
791+
echo "::error::crates.io returned HTTP $code while checking $crate@$VERSION"
792+
exit 1
793+
;;
794+
esac
795+
wait_for_crate "$crate"
753796
done
754-
if [ -n "$FAILED" ]; then
755-
echo "::warning::crates.io publish incomplete for:$FAILED — does NOT block the release; rotate CARGO_REGISTRY_TOKEN / fix version requirements to resolve."
756-
fi
757797
758798
# ── Update Homebrew Formula ────────────────────────────────────
759799
publish-homebrew:

src/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,9 @@ base64 = "0.22"
125125
ring = "0.17"
126126

127127
# Shared types (transport protocol, privacy, tools)
128-
a3s-acl = { git = "https://github.com/A3S-Lab/ACL.git", rev = "fc041758758eba89dd5d22a3414d884c158c9ac1" }
128+
a3s-acl = { version = "0.2.2", git = "https://github.com/A3S-Lab/ACL.git", rev = "fc041758758eba89dd5d22a3414d884c158c9ac1" }
129129
a3s-transport = { version = "0.1.1", package = "a3s-common" }
130-
a3s-runtime = { git = "https://github.com/A3S-Lab/Runtime.git", rev = "91a1b4c67ef2691915f2409a8cf835f7534020ff" }
130+
a3s-runtime = { version = "0.2.0", git = "https://github.com/A3S-Lab/Runtime.git", rev = "91a1b4c67ef2691915f2409a8cf835f7534020ff" }
131131

132132
# Metrics
133133
prometheus = "0.13"

0 commit comments

Comments
 (0)