diff --git a/.github/workflows/release-80_publish-crates.yml b/.github/workflows/release-80_publish-crates.yml new file mode 100644 index 0000000000000..661d838d3b6ce --- /dev/null +++ b/.github/workflows/release-80_publish-crates.yml @@ -0,0 +1,229 @@ +name: Release - Publish Crates + +on: + workflow_dispatch: + inputs: + release_name: + description: 'Release name (e.g., stable2509-3). Base branch is derived by removing the last -N suffix.' + required: true + type: string + registry: + description: 'Registry to publish crates to' + required: true + type: choice + options: + - staging.crates.io + - crates.io + default: staging.crates.io + dry_run: + description: 'Dry run - do not actually publish crates' + required: true + type: boolean + default: true + +permissions: + contents: write + +jobs: + set-image: + runs-on: ubuntu-latest + outputs: + IMAGE: ${{ steps.set_image.outputs.IMAGE }} + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - id: set_image + run: cat .github/env >> $GITHUB_OUTPUT + + publish-crates: + needs: set-image + runs-on: ubuntu-latest + environment: release + env: + PGP_KMS_KEY: ${{ secrets.PGP_KMS_SIGN_COMMITS_KEY }} + PGP_KMS_HASH: ${{ secrets.PGP_KMS_HASH }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + container: + image: ${{ needs.set-image.outputs.IMAGE }} + + steps: + - name: Install pgpkms + run: | + # Install pgpkms that is used to sign commits + pip install git+https://github.com/paritytech-release/pgpkms.git@6cb1cecce1268412189b77e4b130f4fa248c4151 + + - name: Derive stable branch from release name + id: derive_branch + run: | + RELEASE_NAME="${{ inputs.release_name }}" + echo "Release name: $RELEASE_NAME" + + # Extract stable branch by removing the last -N suffix + # e.g., stable2509-3 -> stable2509 + if [[ "$RELEASE_NAME" =~ ^(.+)-[0-9]+$ ]]; then + STABLE_BRANCH="${BASH_REMATCH[1]}" + else + # If no suffix, use the release name as-is (first release) + STABLE_BRANCH="$RELEASE_NAME" + fi + + echo "Stable branch: $STABLE_BRANCH" + echo "STABLE_BRANCH=$STABLE_BRANCH" >> $GITHUB_OUTPUT + + # I am calling it like this because we will call post crates workflow after on this one + echo "RELEASE_BRANCH=post-crates-release-$RELEASE_NAME" >> $GITHUB_OUTPUT + + - name: Checkout stable branch + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ steps.derive_branch.outputs.STABLE_BRANCH }} + fetch-depth: 0 + + - name: Import GPG keys + shell: bash + run: | + . ./.github/scripts/common/lib.sh + import_gpg_keys + + - name: Configure git + shell: bash + run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" + git config --global commit.gpgsign true + PGPKMS_PATH=$(which pgpkms-git) + echo "Using pgpkms-git at: $PGPKMS_PATH" + git config --global gpg.program "$PGPKMS_PATH" + git config --global user.name "ParityReleases" + git config --global user.email "release-team@parity.io" + git config --global user.signingKey "D8018FBB3F534D866A45998293C5FB5F6A367B51" + + - name: Create release branch + run: | + RELEASE_BRANCH="${{ steps.derive_branch.outputs.RELEASE_BRANCH }}" + echo "Creating branch: $RELEASE_BRANCH" + + git checkout -b "$RELEASE_BRANCH" + echo "Successfully created branch $RELEASE_BRANCH" + + - name: Rust Cache + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + cache-on-failure: true + + - name: Install parity-publish + run: cargo install parity-publish --locked -q + + - name: Run parity-publish plan + run: | + echo "Running parity-publish plan..." + parity-publish plan --prdoc prdoc + + - name: Save Plan.toml diff + run: | + RELEASE_NAME="${{ inputs.release_name }}" + mkdir -p release-artifacts + + echo "Saving Plan.toml diff..." + git diff Plan.toml > "release-artifacts/changed_crates_${RELEASE_NAME}.txt" + + echo "Plan.toml changes:" + cat "release-artifacts/changed_crates_${RELEASE_NAME}.txt" + + - name: Parse crate names for release notes + run: | + RELEASE_NAME="${{ inputs.release_name }}" + + echo "Parsing crate names..." + python3 scripts/release/parse-crates-names.py \ + "release-artifacts/changed_crates_${RELEASE_NAME}.txt" \ + scripts/release/templates/crates_list.md.tera + + echo "Crates list:" + cat scripts/release/templates/crates_list.md.tera + + - name: Commit Plan.toml and crates list + shell: bash + run: | + . ./.github/scripts/release/release_lib.sh + + git add Plan.toml scripts/release/templates/crates_list.md.tera + + if [[ -n $(git status --porcelain) ]]; then + commit_with_message "chore: update Plan.toml and crates list for ${{ inputs.release_name }}" + echo "Committed Plan.toml and crates list" + else + echo "No changes to commit" + fi + + - name: Run parity-publish apply + run: | + echo "Running parity-publish apply..." + parity-publish apply + + - name: Update Cargo.lock + run: | + echo "Updating Cargo.lock..." + cargo update --workspace --offline || cargo update --workspace + echo "Cargo.lock updated" + + - name: Commit version bumps + shell: bash + run: | + . ./.github/scripts/release/release_lib.sh + + git add -A + + if [[ -n $(git status --porcelain) ]]; then + commit_with_message "chore: apply version bumps for ${{ inputs.release_name }}" + echo "Committed version bumps" + else + echo "No changes to commit" + fi + + - name: Push release branch + run: | + RELEASE_BRANCH="${{ steps.derive_branch.outputs.RELEASE_BRANCH }}" + echo "Pushing branch $RELEASE_BRANCH..." + git push origin "$RELEASE_BRANCH" + echo "Successfully pushed $RELEASE_BRANCH" + + - name: Configure cargo registry + run: | + REGISTRY="${{ inputs.registry }}" + echo "Configuring cargo for $REGISTRY..." + mkdir -p ~/.cargo + + if [ "$REGISTRY" = "staging.crates.io" ]; then + cat >> ~/.cargo/config.toml << 'EOF' + [registries.crates-io] + index = "sparse+https://index.staging.crates.io/" + EOF + else + echo "Using default crates.io registry" + fi + + echo "Cargo config:" + cat ~/.cargo/config.toml || echo "(using defaults)" + + - name: Publish crates + env: + PARITY_PUBLISH_CRATESIO_TOKEN: ${{ inputs.registry == 'staging.crates.io' && secrets.STAGING_CRATESIO_PUBLISH_TOKEN || secrets.CRATESIO_PUBLISH_TOKEN }} + run: | + DRY_RUN="${{ inputs.dry_run }}" + REGISTRY="${{ inputs.registry }}" + + if [ "$DRY_RUN" = "true" ]; then + echo "DRY RUN - Not actually publishing crates" + echo "Target registry: $REGISTRY" + echo "Would run: parity-publish apply -p --batch-delay 15 --max-concurrent 1 --batch-size 1" + echo "" + echo "Crates that would be published:" + parity-publish apply --print || true + else + echo "Publishing crates to $REGISTRY..." + parity-publish apply -p --batch-delay 15 --max-concurrent 1 --batch-size 1 + echo "Crates published successfully to $REGISTRY!" + fi diff --git a/Cargo.lock b/Cargo.lock index c8658f9c4e892..2be92d8a096ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14457,6 +14457,25 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "parity-staging-test-a" +version = "0.2.0" + +[[package]] +name = "parity-staging-test-b" +version = "0.1.0" +dependencies = [ + "parity-staging-test-a", +] + +[[package]] +name = "parity-staging-test-c" +version = "0.1.0" +dependencies = [ + "parity-staging-test-a", + "parity-staging-test-b", +] + [[package]] name = "parity-wasm" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index a873db170b9f2..d723c047d8ef7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -578,6 +578,11 @@ members = [ "templates/solochain/runtime", "templates/zombienet", "umbrella", + + # Test crates for staging.crates.io validation (temporary) + "test-crates/parity-staging-test-a", + "test-crates/parity-staging-test-b", + "test-crates/parity-staging-test-c", ] default-members = [ diff --git a/scripts/release/test-staging-publish.sh b/scripts/release/test-staging-publish.sh new file mode 100755 index 0000000000000..e186d12b578bf --- /dev/null +++ b/scripts/release/test-staging-publish.sh @@ -0,0 +1,214 @@ +#!/bin/bash +# +# Test script for publishing crates to staging.crates.io +# +# This script allows you to test the crate publishing flow without affecting +# production crates.io. It uses environment variables to redirect cargo to +# staging.crates.io instead of modifying your local cargo config. +# +# Usage: +# ./scripts/release/test-staging-publish.sh [options] +# +# Options: +# --dry-run Don't actually publish, just show what would be published +# --token TOKEN crates.io API token (or set STAGING_CRATESIO_TOKEN env var) +# --crates LIST Comma-separated list of crates to publish (default: test crates) +# --help Show this help message +# +# Examples: +# # Dry run with test crates +# ./scripts/release/test-staging-publish.sh --dry-run +# +# # Publish test crates to staging +# ./scripts/release/test-staging-publish.sh --token YOUR_TOKEN +# +# # Publish specific crates +# ./scripts/release/test-staging-publish.sh --crates "parity-staging-test-a,parity-staging-test-b" +# + +set -e + +# Default values +DRY_RUN=false +TOKEN="${STAGING_CRATESIO_TOKEN:-}" +CRATES="parity-staging-test-a,parity-staging-test-b,parity-staging-test-c" +PARITY_PUBLISH_PATH="${PARITY_PUBLISH_PATH:-../parity-publish/target/release/parity-publish}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +print_help() { + sed -n '2,/^$/p' "$0" | sed 's/^# //' | sed 's/^#//' +} + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --token) + TOKEN="$2" + shift 2 + ;; + --crates) + CRATES="$2" + shift 2 + ;; + --parity-publish) + PARITY_PUBLISH_PATH="$2" + shift 2 + ;; + --help|-h) + print_help + exit 0 + ;; + *) + log_error "Unknown option: $1" + print_help + exit 1 + ;; + esac +done + +# Check parity-publish exists +if [[ ! -x "$PARITY_PUBLISH_PATH" ]]; then + log_error "parity-publish not found at: $PARITY_PUBLISH_PATH" + log_info "Set PARITY_PUBLISH_PATH environment variable or use --parity-publish flag" + log_info "Example: export PARITY_PUBLISH_PATH=/path/to/parity-publish" + exit 1 +fi + +log_info "Using parity-publish at: $PARITY_PUBLISH_PATH" + +# Check token +if [[ -z "$TOKEN" ]] && [[ "$DRY_RUN" == "false" ]]; then + log_error "No token provided. Use --token or set STAGING_CRATESIO_TOKEN env var" + log_info "For dry run, use --dry-run flag" + exit 1 +fi + +# Create a temporary mini-workspace with only the test crates. +# This avoids the full polkadot-sdk workspace resolution, which would try to +# resolve all 500+ crates (scale-info, etc.) against staging and fail. +REPO_ROOT="$(pwd)" +TEMP_WORKSPACE=$(mktemp -d) + +cleanup_temp() { + rm -rf "$TEMP_WORKSPACE" + log_info "Cleaned up temporary workspace" +} +trap cleanup_temp EXIT + +# Copy ALL test crates into the temp workspace (not just the ones being published) +# so that path dependencies between them resolve correctly. +IFS=',' read -ra CRATE_ARRAY <<< "$CRATES" + +MEMBERS="" +for crate_dir in "$REPO_ROOT"/test-crates/*/; do + crate_name=$(basename "$crate_dir") + cp -r "$crate_dir" "$TEMP_WORKSPACE/$crate_name" + MEMBERS="$MEMBERS\"$crate_name\"," +done + +# Create temp workspace Cargo.toml +cat > "$TEMP_WORKSPACE/Cargo.toml" << EOF +[workspace] +members = [${MEMBERS}] +resolver = "2" +EOF + +# Create .cargo/config.toml: +# - [source] replacement so dependency resolution uses staging +# - [registries] so cargo publish uploads to staging +mkdir -p "$TEMP_WORKSPACE/.cargo" +cat > "$TEMP_WORKSPACE/.cargo/config.toml" << EOF +[source.crates-io] +replace-with = "staging" + +[source.staging] +registry = "sparse+https://index.staging.crates.io/" + +[registries.staging] +index = "sparse+https://index.staging.crates.io/" +EOF + +log_info "Created temporary workspace at: $TEMP_WORKSPACE" +log_info "Configured to publish to: staging.crates.io" +log_info "Crates to publish: $CRATES" +log_info "Dry run: $DRY_RUN" +echo "" + +if [[ "$DRY_RUN" == "true" ]]; then + log_info "=== DRY RUN MODE ===" + log_info "Would publish the following crates to staging.crates.io:" + echo "" + for crate in "${CRATE_ARRAY[@]}"; do + echo " - $crate" + done + echo "" + log_info "To actually publish, run without --dry-run flag" +else + log_info "=== PUBLISHING TO STAGING.CRATES.IO ===" + echo "" + + # Fix path dependencies to be relative within the temp workspace + for crate in "${CRATE_ARRAY[@]}"; do + TOML="$TEMP_WORKSPACE/$crate/Cargo.toml" + # Rewrite path deps to point within the temp workspace + for dep_crate in "${CRATE_ARRAY[@]}"; do + if [[ "$crate" != "$dep_crate" ]]; then + sed -i.bak "s|path = \"../[^\"]*$dep_crate\"|path = \"../$dep_crate\"|g" "$TOML" + rm -f "${TOML}.bak" + fi + done + done + + # Publish from the temp workspace so cargo picks up its .cargo/config.toml + pushd "$TEMP_WORKSPACE" > /dev/null + + # Publish each crate in order (respecting dependencies) + for crate in "${CRATE_ARRAY[@]}"; do + log_info "Publishing $crate..." + + cargo publish \ + -p "$crate" \ + --registry staging \ + --token "$TOKEN" \ + --allow-dirty \ + --no-verify \ + 2>&1 || { + log_error "Failed to publish $crate" + popd > /dev/null + exit 1 + } + + log_info "Successfully published $crate" + + # Wait a bit between publishes to avoid rate limiting + log_info "Waiting 30 seconds before next publish..." + sleep 30 + done + + popd > /dev/null + + echo "" + log_info "=== ALL CRATES PUBLISHED SUCCESSFULLY ===" + log_info "Check them at: https://staging.crates.io" +fi diff --git a/test-crates/parity-staging-test-a/Cargo.toml b/test-crates/parity-staging-test-a/Cargo.toml new file mode 100644 index 0000000000000..ebf9f62862eb0 --- /dev/null +++ b/test-crates/parity-staging-test-a/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "parity-staging-test-a" +version = "0.2.0" +edition = "2021" +description = "Test crate A for staging.crates.io publishing validation" +license = "Apache-2.0" +repository = "https://github.com/paritytech/polkadot-sdk" + +[dependencies] diff --git a/test-crates/parity-staging-test-a/src/lib.rs b/test-crates/parity-staging-test-a/src/lib.rs new file mode 100644 index 0000000000000..a39f98ea0f4de --- /dev/null +++ b/test-crates/parity-staging-test-a/src/lib.rs @@ -0,0 +1,14 @@ +/// A simple function for testing +pub fn hello_from_a() -> &'static str { + "Hello from crate A!" +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + assert_eq!(hello_from_a(), "Hello from crate A!"); + } +} diff --git a/test-crates/parity-staging-test-b/Cargo.toml b/test-crates/parity-staging-test-b/Cargo.toml new file mode 100644 index 0000000000000..93bf9822c2fa5 --- /dev/null +++ b/test-crates/parity-staging-test-b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "parity-staging-test-b" +version = "0.1.0" +edition = "2021" +description = "Test crate B for staging.crates.io publishing validation - depends on A" +license = "Apache-2.0" +repository = "https://github.com/paritytech/polkadot-sdk" + +[dependencies] +parity-staging-test-a = { path = "../parity-staging-test-a", version = "0.2.0" } diff --git a/test-crates/parity-staging-test-b/src/lib.rs b/test-crates/parity-staging-test-b/src/lib.rs new file mode 100644 index 0000000000000..1c3d6f874e7ec --- /dev/null +++ b/test-crates/parity-staging-test-b/src/lib.rs @@ -0,0 +1,17 @@ +use parity_staging_test_a::hello_from_a; + +/// A function that uses crate A +pub fn hello_from_b() -> String { + format!("Hello from crate B! Also, {}", hello_from_a()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + assert!(hello_from_b().contains("crate B")); + assert!(hello_from_b().contains("crate A")); + } +} diff --git a/test-crates/parity-staging-test-c/Cargo.toml b/test-crates/parity-staging-test-c/Cargo.toml new file mode 100644 index 0000000000000..9fbd284ddf7d5 --- /dev/null +++ b/test-crates/parity-staging-test-c/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "parity-staging-test-c" +version = "0.1.0" +edition = "2021" +description = "Test crate C for staging.crates.io publishing validation - depends on A and B" +license = "Apache-2.0" +repository = "https://github.com/paritytech/polkadot-sdk" + +[dependencies] +parity-staging-test-a = { path = "../parity-staging-test-a", version = "0.2.0" } +parity-staging-test-b = { path = "../parity-staging-test-b", version = "0.1.0" } diff --git a/test-crates/parity-staging-test-c/src/lib.rs b/test-crates/parity-staging-test-c/src/lib.rs new file mode 100644 index 0000000000000..eff5377e67cf4 --- /dev/null +++ b/test-crates/parity-staging-test-c/src/lib.rs @@ -0,0 +1,24 @@ +use parity_staging_test_a::hello_from_a; +use parity_staging_test_b::hello_from_b; + +/// A function that uses both crate A and B +pub fn hello_from_c() -> String { + format!( + "Hello from crate C! I can use A directly: '{}' and B: '{}'", + hello_from_a(), + hello_from_b() + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = hello_from_c(); + assert!(result.contains("crate C")); + assert!(result.contains("crate A")); + assert!(result.contains("crate B")); + } +}