diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1da408a..fd5f881 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,16 @@ on: required: true default: true type: boolean + packages: + description: "Comma-separated package list; empty publishes the full workspace" + required: false + default: "" + type: string + skip_published: + description: "Skip versions that already exist on crates.io during a real publish" + required: true + default: true + type: boolean permissions: contents: read @@ -19,11 +29,166 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: verify workspace publish - if: inputs.dry_run - run: cargo publish --workspace --dry-run --locked - - name: publish workspace crates - if: ${{ !inputs.dry_run }} + - name: publish crates env: + DRY_RUN: ${{ inputs.dry_run }} + SELECTED_PACKAGES: ${{ inputs.packages }} + SKIP_PUBLISHED: ${{ inputs.skip_published }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - run: cargo publish --workspace --locked --token "$CARGO_REGISTRY_TOKEN" + run: | + set -euo pipefail + + ordered_packages=( + aelf-proto + aelf-crypto + aelf-client + aelf-keystore + aelf-contract + aelf-sdk + ) + + declare -A allowed_packages=() + declare -A requested_packages=() + + for pkg in "${ordered_packages[@]}"; do + allowed_packages["$pkg"]=1 + done + + if [[ -n "${SELECTED_PACKAGES//[[:space:]]/}" ]]; then + IFS=',' read -r -a raw_packages <<< "$SELECTED_PACKAGES" + for raw_pkg in "${raw_packages[@]}"; do + pkg="${raw_pkg//[[:space:]]/}" + if [[ -z "$pkg" ]]; then + continue + fi + if [[ -z "${allowed_packages[$pkg]:-}" ]]; then + echo "::error::Unsupported package '$pkg'. Allowed packages: ${ordered_packages[*]}" + exit 1 + fi + requested_packages["$pkg"]=1 + done + + if [[ "${#requested_packages[@]}" -eq 0 ]]; then + echo "::error::No valid packages were provided." + exit 1 + fi + fi + + package_selected() { + local pkg="$1" + if [[ "${#requested_packages[@]}" -eq 0 ]]; then + return 0 + fi + [[ -n "${requested_packages[$pkg]:-}" ]] + } + + crate_version() { + local pkgid + local version + + pkgid="$(cargo pkgid -p "$1")" + version="${pkgid##*@}" + if [[ "$version" == "$pkgid" ]]; then + version="${pkgid##*#}" + fi + echo "$version" + } + + crate_registry_state() { + local pkg="$1" + local version="$2" + local output + local cmd_status + + set +e + output="$(cargo info "${pkg}@${version}" --registry crates-io 2>&1)" + cmd_status=$? + set -e + + if [[ "$cmd_status" -eq 0 ]]; then + echo "present" + return 0 + fi + + if grep -qi "could not find" <<< "$output"; then + echo "missing" + return 0 + fi + + echo "::error::Failed to query crates.io index for ${pkg} ${version}: ${output}" >&2 + return 1 + } + + ensure_crate_visible() { + local pkg="$1" + local version="$2" + local max_attempts=30 + local sleep_seconds=10 + + for ((attempt = 1; attempt <= max_attempts; attempt++)); do + state="$(crate_registry_state "$pkg" "$version")" + case "$state" in + present) + echo "${pkg} ${version} is visible on crates.io." + return 0 + ;; + missing) + echo "Waiting for ${pkg} ${version} to become visible on crates.io (${attempt}/${max_attempts})..." + sleep "$sleep_seconds" + ;; + *) + echo "::error::Unexpected crates.io registry state '${state}' while checking ${pkg} ${version}." + return 1 + ;; + esac + done + + echo "::error::Timed out waiting for ${pkg} ${version} to become visible on crates.io." + return 1 + } + + if [[ "$DRY_RUN" == "true" && "${#requested_packages[@]}" -eq 0 ]]; then + echo "Running workspace dry-run for the full publish set." + cargo publish --workspace --dry-run --locked + exit 0 + fi + + if [[ "$DRY_RUN" != "true" && -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then + echo "::error::CARGO_REGISTRY_TOKEN is required when dry_run=false." + exit 1 + fi + + for pkg in "${ordered_packages[@]}"; do + if ! package_selected "$pkg"; then + continue + fi + + version="$(crate_version "$pkg")" + echo "Processing ${pkg} ${version}" + + if [[ "$DRY_RUN" == "true" ]]; then + cargo publish -p "$pkg" --dry-run --locked + continue + fi + + state="$(crate_registry_state "$pkg" "$version")" + case "$state" in + present) + if [[ "$SKIP_PUBLISHED" == "true" ]]; then + echo "Skipping ${pkg} ${version}; version already exists on crates.io." + continue + fi + echo "::error::${pkg} ${version} already exists on crates.io." + exit 1 + ;; + missing) + ;; + *) + echo "::error::Unexpected crates.io registry state '${state}' while checking ${pkg} ${version}." + exit 1 + ;; + esac + + cargo publish -p "$pkg" --locked --token "$CARGO_REGISTRY_TOKEN" + ensure_crate_visible "$pkg" "$version" + done diff --git a/Cargo.lock b/Cargo.lock index 45eb866..66c9a11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,7 @@ dependencies = [ "prost", "prost-build", "prost-types", + "protoc-bin-vendored", "serde", ] @@ -1537,6 +1538,70 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + [[package]] name = "quinn" version = "0.11.9" diff --git a/Cargo.toml b/Cargo.toml index c69a97c..051110d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ prost = "0.14.3" prost-build = "0.14.3" prost-reflect = { version = "0.16.3", features = ["serde"] } prost-types = "0.14.3" +protoc-bin-vendored = "3.2.0" rand = "0.9.2" reqwest = { version = "0.13.2", default-features = false, features = ["json", "query", "rustls", "http2"] } salsa20 = "0.10.2" diff --git a/README.md b/README.md index ee587e4..46e6335 100644 --- a/README.md +++ b/README.md @@ -329,8 +329,15 @@ Release flow: 1. Sync upstream proto files with `scripts/sync_proto.sh` 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` -3. Review `CHANGELOG.md`, then run the manual `publish` GitHub Actions workflow with `dry_run=true` -4. Re-run the `publish` workflow with `dry_run=false` after confirming the crates.io token is configured +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. +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. +5. If crates.io returns a transient error after some crates are already published, rerun the workflow with `dry_run=false`, `packages=`, and `skip_published=true`. For the March 10, 2026 incident, the recovery command is `packages=aelf-sdk`. + +Publishing notes: + +- The publish workflow releases crates in dependency order: `aelf-proto`, `aelf-crypto`, `aelf-client`, `aelf-keystore`, `aelf-contract`, `aelf-sdk`. +- Full-workspace dry-runs still use `cargo publish --workspace --dry-run --locked` so unpublished interdependent versions can be validated together. +- crates.io releases are immutable. If a published version is wrong, it must be `yank`ed and replaced with a new version. CI is defined in `.github/workflows/ci.yml`. Publishing is defined in `.github/workflows/publish.yml` and expects the `CARGO_REGISTRY_TOKEN` repository secret. diff --git a/README.zh.md b/README.zh.md index 6c383b9..36407cf 100644 --- a/README.zh.md +++ b/README.zh.md @@ -329,8 +329,15 @@ tests/fixtures/ 1. 用 `scripts/sync_proto.sh` 同步 upstream proto 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` -3. 检查 `CHANGELOG.md`,然后先手动运行一次 `publish` GitHub Actions workflow,并把 `dry_run` 设为 `true` -4. 确认 crates.io token 已配置后,再以 `dry_run=false` 重新运行 `publish` workflow +3. 检查 `CHANGELOG.md`,然后先手动运行一次 `publish` GitHub Actions workflow,并把 `dry_run` 设为 `true`。全量发版时保持 `packages` 为空;如果只是验证恢复路径,可设置 `packages=aelf-sdk` +4. 确认 crates.io token 已配置后,再以 `dry_run=false` 重新运行 `publish` workflow。保留 `skip_published=true`,这样在部分发布成功后可以安全重试 +5. 如果 crates.io 在部分 crate 已发布后返回瞬时错误,重新运行 workflow,并设置 `dry_run=false`、`packages=<剩余-crates>`、`skip_published=true`。针对 2026 年 3 月 10 日这次事故,恢复时应使用 `packages=aelf-sdk` + +发布说明: + +- 发布 workflow 会按依赖顺序发布:`aelf-proto`、`aelf-crypto`、`aelf-client`、`aelf-keystore`、`aelf-contract`、`aelf-sdk` +- 全量 dry-run 仍然使用 `cargo publish --workspace --dry-run --locked`,这样可以一起验证尚未发布但彼此依赖的 workspace 版本 +- crates.io 已发布版本不可覆盖;如果某个版本发布内容有误,只能先 `yank`,再发布新版本 CI 定义在 `.github/workflows/ci.yml`。 发布流程定义在 `.github/workflows/publish.yml`,需要配置仓库 secret `CARGO_REGISTRY_TOKEN`。 diff --git a/crates/aelf-proto/Cargo.toml b/crates/aelf-proto/Cargo.toml index f91d1e7..ae45b04 100644 --- a/crates/aelf-proto/Cargo.toml +++ b/crates/aelf-proto/Cargo.toml @@ -20,6 +20,7 @@ pbjson-build.workspace = true prost.workspace = true prost-build.workspace = true prost-types.workspace = true +protoc-bin-vendored.workspace = true [lints] workspace = true diff --git a/crates/aelf-proto/build.rs b/crates/aelf-proto/build.rs index 027b117..2093a35 100644 --- a/crates/aelf-proto/build.rs +++ b/crates/aelf-proto/build.rs @@ -5,6 +5,8 @@ use std::path::PathBuf; fn main() -> Result<(), Box> { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let proto_root = manifest_dir.join("proto/upstream"); + let protoc = protoc_bin_vendored::protoc_bin_path()?; + let protoc_include = protoc_bin_vendored::include_path()?; let mut files = collect_proto_files(&proto_root)?; files.sort(); @@ -20,7 +22,8 @@ fn main() -> Result<(), Box> { config.compile_well_known_types(); config.extern_path(".google.protobuf", "::pbjson_types"); config.include_file("_includes.rs"); - config.compile_protos(&files, &[proto_root])?; + config.protoc_executable(protoc); + config.compile_protos(&files, &[proto_root, protoc_include])?; let descriptor_bytes = fs::read(&descriptor_path)?; let packages = collect_packages(&descriptor_bytes)?;