Skip to content

Commit 082f009

Browse files
committed
ci(publish): 🚀 make crates publish workflow resumable
1 parent b57ad11 commit 082f009

File tree

3 files changed

+170
-10
lines changed

3 files changed

+170
-10
lines changed

.github/workflows/publish.yml

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ on:
88
required: true
99
default: true
1010
type: boolean
11+
packages:
12+
description: "Comma-separated package list; empty publishes the full workspace"
13+
required: false
14+
default: ""
15+
type: string
16+
skip_published:
17+
description: "Skip versions that already exist on crates.io during a real publish"
18+
required: true
19+
default: true
20+
type: boolean
1121

1222
permissions:
1323
contents: read
@@ -19,11 +29,147 @@ jobs:
1929
- uses: actions/checkout@v4
2030
- uses: dtolnay/rust-toolchain@stable
2131
- uses: Swatinem/rust-cache@v2
22-
- name: verify workspace publish
23-
if: inputs.dry_run
24-
run: cargo publish --workspace --dry-run --locked
25-
- name: publish workspace crates
26-
if: ${{ !inputs.dry_run }}
32+
- name: publish crates
2733
env:
34+
DRY_RUN: ${{ inputs.dry_run }}
35+
SELECTED_PACKAGES: ${{ inputs.packages }}
36+
SKIP_PUBLISHED: ${{ inputs.skip_published }}
2837
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
29-
run: cargo publish --workspace --locked --token "$CARGO_REGISTRY_TOKEN"
38+
run: |
39+
set -euo pipefail
40+
41+
ordered_packages=(
42+
aelf-proto
43+
aelf-crypto
44+
aelf-client
45+
aelf-keystore
46+
aelf-contract
47+
aelf-sdk
48+
)
49+
50+
declare -A allowed_packages=()
51+
declare -A requested_packages=()
52+
53+
for pkg in "${ordered_packages[@]}"; do
54+
allowed_packages["$pkg"]=1
55+
done
56+
57+
if [[ -n "${SELECTED_PACKAGES//[[:space:]]/}" ]]; then
58+
IFS=',' read -r -a raw_packages <<< "$SELECTED_PACKAGES"
59+
for raw_pkg in "${raw_packages[@]}"; do
60+
pkg="${raw_pkg//[[:space:]]/}"
61+
if [[ -z "$pkg" ]]; then
62+
continue
63+
fi
64+
if [[ -z "${allowed_packages[$pkg]:-}" ]]; then
65+
echo "::error::Unsupported package '$pkg'. Allowed packages: ${ordered_packages[*]}"
66+
exit 1
67+
fi
68+
requested_packages["$pkg"]=1
69+
done
70+
71+
if [[ "${#requested_packages[@]}" -eq 0 ]]; then
72+
echo "::error::No valid packages were provided."
73+
exit 1
74+
fi
75+
fi
76+
77+
package_selected() {
78+
local pkg="$1"
79+
if [[ "${#requested_packages[@]}" -eq 0 ]]; then
80+
return 0
81+
fi
82+
[[ -n "${requested_packages[$pkg]:-}" ]]
83+
}
84+
85+
crate_version() {
86+
cargo pkgid -p "$1" | sed 's/.*@//'
87+
}
88+
89+
crate_status_code() {
90+
local pkg="$1"
91+
local version="$2"
92+
curl -sS -o /dev/null -w '%{http_code}' "https://crates.io/api/v1/crates/${pkg}/${version}"
93+
}
94+
95+
ensure_crate_visible() {
96+
local pkg="$1"
97+
local version="$2"
98+
local max_attempts=30
99+
local sleep_seconds=10
100+
101+
for ((attempt = 1; attempt <= max_attempts; attempt++)); do
102+
http_code="$(crate_status_code "$pkg" "$version")"
103+
case "$http_code" in
104+
200)
105+
echo "${pkg} ${version} is visible on crates.io."
106+
return 0
107+
;;
108+
404)
109+
echo "Waiting for ${pkg} ${version} to become visible on crates.io (${attempt}/${max_attempts})..."
110+
sleep "$sleep_seconds"
111+
;;
112+
5??)
113+
echo "::error::crates.io returned ${http_code} while checking ${pkg} ${version}."
114+
return 1
115+
;;
116+
*)
117+
echo "::error::Unexpected crates.io status ${http_code} while checking ${pkg} ${version}."
118+
return 1
119+
;;
120+
esac
121+
done
122+
123+
echo "::error::Timed out waiting for ${pkg} ${version} to become visible on crates.io."
124+
return 1
125+
}
126+
127+
if [[ "$DRY_RUN" == "true" && "${#requested_packages[@]}" -eq 0 ]]; then
128+
echo "Running workspace dry-run for the full publish set."
129+
cargo publish --workspace --dry-run --locked
130+
exit 0
131+
fi
132+
133+
if [[ "$DRY_RUN" != "true" && -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
134+
echo "::error::CARGO_REGISTRY_TOKEN is required when dry_run=false."
135+
exit 1
136+
fi
137+
138+
for pkg in "${ordered_packages[@]}"; do
139+
if ! package_selected "$pkg"; then
140+
continue
141+
fi
142+
143+
version="$(crate_version "$pkg")"
144+
echo "Processing ${pkg} ${version}"
145+
146+
if [[ "$DRY_RUN" == "true" ]]; then
147+
cargo publish -p "$pkg" --dry-run --locked
148+
continue
149+
fi
150+
151+
http_code="$(crate_status_code "$pkg" "$version")"
152+
case "$http_code" in
153+
200)
154+
if [[ "$SKIP_PUBLISHED" == "true" ]]; then
155+
echo "Skipping ${pkg} ${version}; version already exists on crates.io."
156+
continue
157+
fi
158+
echo "::error::${pkg} ${version} already exists on crates.io."
159+
exit 1
160+
;;
161+
404)
162+
;;
163+
5??)
164+
echo "::error::crates.io returned ${http_code} while checking ${pkg} ${version}."
165+
exit 1
166+
;;
167+
*)
168+
echo "::error::Unexpected crates.io status ${http_code} while checking ${pkg} ${version}."
169+
exit 1
170+
;;
171+
esac
172+
173+
cargo publish -p "$pkg" --locked --token "$CARGO_REGISTRY_TOKEN"
174+
ensure_crate_visible "$pkg" "$version"
175+
done

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,15 @@ Release flow:
329329

330330
1. Sync upstream proto files with `scripts/sync_proto.sh`
331331
2. Run `cargo fmt`, `cargo +1.85.0 check --workspace --all-targets --all-features --locked`, `cargo clippy --workspace --all-targets --all-features`, `cargo audit`, `cargo check --workspace --examples`, `cargo test --workspace`
332-
3. Review `CHANGELOG.md`, then run the manual `publish` GitHub Actions workflow with `dry_run=true`
333-
4. Re-run the `publish` workflow with `dry_run=false` after confirming the crates.io token is configured
332+
3. Review `CHANGELOG.md`, then run the manual `publish` GitHub Actions workflow with `dry_run=true`. Leave `packages` empty for a full release dry-run, or set `packages=aelf-sdk` to verify a targeted recovery path.
333+
4. Re-run the `publish` workflow with `dry_run=false` after confirming the crates.io token is configured. Leave `skip_published=true` so retries can safely resume after a partial publish.
334+
5. If crates.io returns a transient error after some crates are already published, rerun the workflow with `dry_run=false`, `packages=<remaining-crates>`, and `skip_published=true`. For the March 10, 2026 incident, the recovery command is `packages=aelf-sdk`.
335+
336+
Publishing notes:
337+
338+
- The publish workflow releases crates in dependency order: `aelf-proto`, `aelf-crypto`, `aelf-client`, `aelf-keystore`, `aelf-contract`, `aelf-sdk`.
339+
- Full-workspace dry-runs still use `cargo publish --workspace --dry-run --locked` so unpublished interdependent versions can be validated together.
340+
- crates.io releases are immutable. If a published version is wrong, it must be `yank`ed and replaced with a new version.
334341

335342
CI is defined in `.github/workflows/ci.yml`.
336343
Publishing is defined in `.github/workflows/publish.yml` and expects the `CARGO_REGISTRY_TOKEN` repository secret.

README.zh.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,15 @@ tests/fixtures/
329329

330330
1.`scripts/sync_proto.sh` 同步 upstream proto
331331
2. 执行 `cargo fmt``cargo +1.85.0 check --workspace --all-targets --all-features --locked``cargo clippy --workspace --all-targets --all-features``cargo audit``cargo check --workspace --examples``cargo test --workspace`
332-
3. 检查 `CHANGELOG.md`,然后先手动运行一次 `publish` GitHub Actions workflow,并把 `dry_run` 设为 `true`
333-
4. 确认 crates.io token 已配置后,再以 `dry_run=false` 重新运行 `publish` workflow
332+
3. 检查 `CHANGELOG.md`,然后先手动运行一次 `publish` GitHub Actions workflow,并把 `dry_run` 设为 `true`。全量发版时保持 `packages` 为空;如果只是验证恢复路径,可设置 `packages=aelf-sdk`
333+
4. 确认 crates.io token 已配置后,再以 `dry_run=false` 重新运行 `publish` workflow。保留 `skip_published=true`,这样在部分发布成功后可以安全重试
334+
5. 如果 crates.io 在部分 crate 已发布后返回瞬时错误,重新运行 workflow,并设置 `dry_run=false``packages=<剩余-crates>``skip_published=true`。针对 2026 年 3 月 10 日这次事故,恢复时应使用 `packages=aelf-sdk`
335+
336+
发布说明:
337+
338+
- 发布 workflow 会按依赖顺序发布:`aelf-proto``aelf-crypto``aelf-client``aelf-keystore``aelf-contract``aelf-sdk`
339+
- 全量 dry-run 仍然使用 `cargo publish --workspace --dry-run --locked`,这样可以一起验证尚未发布但彼此依赖的 workspace 版本
340+
- crates.io 已发布版本不可覆盖;如果某个版本发布内容有误,只能先 `yank`,再发布新版本
334341

335342
CI 定义在 `.github/workflows/ci.yml`
336343
发布流程定义在 `.github/workflows/publish.yml`,需要配置仓库 secret `CARGO_REGISTRY_TOKEN`

0 commit comments

Comments
 (0)