From 8e73364f87ed59a84ad58fc943f5847eda000c45 Mon Sep 17 00:00:00 2001 From: aelf-hzz780 Date: Wed, 11 Mar 2026 18:14:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(ci):=20=F0=9F=A9=B9=20automate=20tag=20rele?= =?UTF-8?q?ases=20and=20harden=20public=20smoke=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/publish.yml | 224 +++++++++--------------------- CHANGELOG.md | 1 + CONTRIBUTING.md | 6 + crates/aelf-client/src/dto/mod.rs | 97 ++++++++++++- scripts/publish-crates.sh | 158 +++++++++++++++++++++ 5 files changed, 326 insertions(+), 160 deletions(-) create mode 100755 scripts/publish-crates.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4124781..1b53c85 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,9 @@ name: publish on: + push: + tags: + - "v*" workflow_dispatch: inputs: dry_run: @@ -23,172 +26,79 @@ permissions: contents: read jobs: - publish: + release-preflight: + if: github.event_name == 'push' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip2 - uses: Swatinem/rust-cache@v2 - - 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 }} + - name: validate tag matches workspace version 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 + tag_version="${GITHUB_REF_NAME#v}" + pkgid="$(cargo pkgid -p aelf-sdk)" + workspace_version="${pkgid##*@}" + if [[ "$workspace_version" == "$pkgid" ]]; then + workspace_version="${pkgid##*#}" 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" && -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then - echo "::error::CARGO_REGISTRY_TOKEN is required when dry_run=false." + if [[ "$tag_version" != "$workspace_version" ]]; then + echo "::error::Tag version ${tag_version} does not match workspace version ${workspace_version}." exit 1 fi + - name: install cargo-audit + run: cargo install cargo-audit --locked + - name: fmt + run: cargo fmt --all -- --check + - name: msrv + run: cargo +1.85.0 check --workspace --all-targets --all-features --locked + - name: clippy + run: cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: audit + run: cargo audit + - name: examples + run: cargo check --workspace --examples + - name: wasm aelf-client + run: cargo check -p aelf-client --target wasm32-wasip2 --no-default-features + - name: wasm aelf-contract + run: cargo check -p aelf-contract --target wasm32-wasip2 --no-default-features + - name: wasm aelf-sdk + run: cargo check -p aelf-sdk --target wasm32-wasip2 --no-default-features + - name: test + run: cargo test --workspace + - name: public readonly smoke + run: cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads=1 --nocapture + + tag-publish: + if: github.event_name == 'push' + runs-on: ubuntu-latest + needs: release-preflight + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: publish crates + env: + DRY_RUN: "false" + SELECTED_PACKAGES: "" + SKIP_PUBLISHED: "true" + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: ./scripts/publish-crates.sh - 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 - - 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 + manual-publish: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - 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: ./scripts/publish-crates.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index a7554a5..550b625 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht - The facade crate now exposes the transport abstraction needed by native-wasm skill runtimes - `send_transaction` no longer treats arbitrary non-empty text payloads as success - typed contract wrappers now lazily reuse the first descriptor per handle, while direct `contract_at(...)` calls still fetch a fresh descriptor for each new handle +- `ChainStatusDto` now accepts both public-node branch map shapes, preventing readonly smoke failures behind mixed main-chain gateway backends ## [0.1.0-alpha.0] - 2026-03-10 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ab4954..8506256 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,6 +55,12 @@ cargo test -p aelf-sdk --test public_readonly_smoke -- --ignored --test-threads= - Keep the documented MSRV at Rust `1.85` and preserve the hard `cargo +1.85.0 check --workspace --all-targets --all-features --locked` CI gate. - Preserve the `wasm32-wasip2` compile gates for `aelf-client`, `aelf-contract`, and `aelf-sdk` when changing transport or feature-flag behavior. +## Release Flow + +- Push a tag such as `v0.1.0-alpha.1` to trigger the automated release flow. +- The tag workflow runs release preflight checks first, then publishes the workspace crates in dependency order. +- If a publish run partially succeeds, rerun the `publish` workflow manually with `packages="aelf-contract,aelf-sdk"`-style inputs and keep `skip_published=true`. + ## Commit Style Use Conventional Commit / `git-cz` style messages in English and keep the matching emoji prefix used by the repository workflow. diff --git a/crates/aelf-client/src/dto/mod.rs b/crates/aelf-client/src/dto/mod.rs index 647f033..ecea351 100644 --- a/crates/aelf-client/src/dto/mod.rs +++ b/crates/aelf-client/src/dto/mod.rs @@ -2,6 +2,7 @@ use aelf_proto::aelf::{ResourceTokenCharged, TransactionFeeCharged}; use base64::Engine; use prost::Message; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::{Map, Value}; use std::collections::{BTreeMap, HashMap}; /// Chain status returned by the blockchain status endpoint. @@ -9,9 +10,9 @@ use std::collections::{BTreeMap, HashMap}; #[serde(rename_all = "PascalCase")] pub struct ChainStatusDto { pub chain_id: String, - #[serde(default)] + #[serde(default, deserialize_with = "block_height_map_as_default")] pub branches: HashMap, - #[serde(default)] + #[serde(default, deserialize_with = "block_height_map_as_default")] pub not_linked_blocks: HashMap, pub longest_chain_height: i64, pub longest_chain_hash: String, @@ -249,6 +250,47 @@ where Ok(value.unwrap_or_default()) } +fn block_height_map_as_default<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let value = Option::>::deserialize(deserializer)?; + let mut result = HashMap::new(); + + for (key, value) in value.unwrap_or_default() { + match value { + Value::Number(number) => { + let height = number.as_i64().ok_or_else(|| { + serde::de::Error::custom(format!( + "block height value for '{key}' is not a signed integer" + )) + })?; + result.insert(key, height); + } + Value::String(text) => { + if let Ok(height) = text.parse::() { + result.insert(key, height); + continue; + } + + let inverted_height = key.parse::().map_err(|_| { + serde::de::Error::custom(format!( + "invalid block height map entry '{key}': '{text}'" + )) + })?; + result.insert(text, inverted_height); + } + other => { + return Err(serde::de::Error::custom(format!( + "invalid block height map value for '{key}': {other}" + ))); + } + } + } + + Ok(result) +} + /// Merkle path node returned by transaction proof queries. #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "PascalCase")] @@ -326,10 +368,11 @@ pub struct WebAppError { #[cfg(test)] mod tests { - use super::{LogEventDto, TransactionResultDto}; + use super::{ChainStatusDto, LogEventDto, TransactionResultDto}; use aelf_proto::aelf::{Address, ResourceTokenCharged, TransactionFeeCharged}; use base64::Engine; use prost::Message; + use serde_json::json; #[test] fn parses_transaction_fee_logs() { @@ -374,4 +417,52 @@ mod tests { assert_eq!(fees.get("ELF"), Some(&12_345)); assert_eq!(fees.get("CPU"), Some(&999)); } + + #[test] + fn parses_chain_status_hash_to_height_maps() { + let status: ChainStatusDto = serde_json::from_value(json!({ + "ChainId": "AELF", + "Branches": { + "abc": 42 + }, + "NotLinkedBlocks": null, + "LongestChainHeight": 42, + "LongestChainHash": "abc", + "GenesisBlockHash": "genesis", + "GenesisContractAddress": "contract", + "LastIrreversibleBlockHash": "lib", + "LastIrreversibleBlockHeight": 40, + "BestChainHash": "abc", + "BestChainHeight": 42 + })) + .expect("chain status"); + + assert_eq!(status.branches.get("abc"), Some(&42)); + assert!(status.not_linked_blocks.is_empty()); + } + + #[test] + fn parses_chain_status_height_to_hash_maps() { + let status: ChainStatusDto = serde_json::from_value(json!({ + "ChainId": "AELF", + "Branches": { + "42": "abc" + }, + "NotLinkedBlocks": { + "7": "def" + }, + "LongestChainHeight": 42, + "LongestChainHash": "abc", + "GenesisBlockHash": "genesis", + "GenesisContractAddress": "contract", + "LastIrreversibleBlockHash": "lib", + "LastIrreversibleBlockHeight": 40, + "BestChainHash": "abc", + "BestChainHeight": 42 + })) + .expect("chain status"); + + assert_eq!(status.branches.get("abc"), Some(&42)); + assert_eq!(status.not_linked_blocks.get("def"), Some(&7)); + } } diff --git a/scripts/publish-crates.sh b/scripts/publish-crates.sh new file mode 100755 index 0000000..72c8699 --- /dev/null +++ b/scripts/publish-crates.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash + +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:-}" && -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:-false}" != "true" && -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then + echo "::error::CARGO_REGISTRY_TOKEN is required when DRY_RUN=false." + exit 1 +fi + +if [[ "${DRY_RUN:-false}" == "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 + +for pkg in "${ordered_packages[@]}"; do + if ! package_selected "$pkg"; then + continue + fi + + version="$(crate_version "$pkg")" + echo "Processing ${pkg} ${version}" + + if [[ "${DRY_RUN:-false}" == "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}" == "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