diff --git a/.github/workflows/rust-docker.yml b/.github/workflows/rust-docker.yml index f36c895b24..eb64fb8d60 100644 --- a/.github/workflows/rust-docker.yml +++ b/.github/workflows/rust-docker.yml @@ -48,8 +48,26 @@ jobs: - name: Generate tag data id: taggen run: | + set -euo pipefail echo "TAG_DATE=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT echo "TAG_SHA=$(echo '${{ github.event.pull_request.head.sha || github.sha }}' | cut -b 1-7)" >> $GITHUB_OUTPUT + # For tag events, derive pure semver: + if [ "${{ github.ref_type }}" = "tag" ]; then + NAME="${{ github.ref_name }}" + # Strip agents- prefix and any leading v + NAME="${NAME#agents-}" + NAME="${NAME#v}" + # Basic semver guard (allows prerelease/build metadata) + if echo "$NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'; then + echo "SEMVER=$NAME" >> $GITHUB_OUTPUT + # Check if this is a stable release (no prerelease suffix like -beta, -rc, -alpha) + if echo "$NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "IS_STABLE=true" >> $GITHUB_OUTPUT + else + echo "IS_STABLE=false" >> $GITHUB_OUTPUT + fi + fi + fi - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -61,6 +79,9 @@ jobs: tags: | type=ref,event=branch type=ref,event=pr + type=ref,event=tag + type=semver,pattern={{version}},value=${{ steps.taggen.outputs.SEMVER }},enable=${{ github.ref_type == 'tag' && steps.taggen.outputs.SEMVER != '' }} + type=semver,pattern={{major}}.{{minor}},value=${{ steps.taggen.outputs.SEMVER }},enable=${{ github.ref_type == 'tag' && steps.taggen.outputs.IS_STABLE == 'true' }} type=raw,value=${{ steps.taggen.outputs.TAG_SHA }}-${{ steps.taggen.outputs.TAG_DATE }} - name: Set up Depot CLI uses: depot/setup-action@v1 diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml new file mode 100644 index 0000000000..a62d8a8f8d --- /dev/null +++ b/.github/workflows/rust-release.yml @@ -0,0 +1,374 @@ +name: Rust Agent Release + +on: + push: + branches: + - main + - pb/rust-release-cargo # for testing + paths: + - 'rust/main/**' + - 'rust/scripts/ci/**' + - '.github/workflows/rust-release.yml' + workflow_dispatch: + inputs: + prerelease_suffix: + description: 'Prerelease suffix (e.g., "preview.1", "rc.1", "alpha.2"). Leave empty to auto-generate "preview.N".' + required: false + type: string + default: '' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + +jobs: + check-release-status: + name: Check Release Status + runs-on: depot-ubuntu-latest + outputs: + has_changes: ${{ steps.check_changes.outputs.has_changes }} + should_release: ${{ steps.check_version.outputs.should_release }} + current_version: ${{ steps.check_version.outputs.current_version }} + latest_version: ${{ steps.check_version.outputs.latest_version }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Check if there are changes since last release + id: check_changes + working-directory: ./rust/main + run: | + # Get latest agents-v* tag + LATEST_TAG=$(../scripts/ci/get-latest-agents-tag.sh) + + if [ -z "$LATEST_TAG" ]; then + echo "No previous release found" + echo "has_changes=true" >> $GITHUB_OUTPUT + else + # Check if there are commits to rust/main since last release + COMMITS_SINCE=$(git log "$LATEST_TAG"..HEAD --oneline -- . | wc -l) + echo "Commits since $LATEST_TAG: $COMMITS_SINCE" + + if [ "$COMMITS_SINCE" -gt 0 ]; then + echo "Found $COMMITS_SINCE commit(s) since last release" + echo "has_changes=true" >> $GITHUB_OUTPUT + else + echo "No commits since last release" + echo "has_changes=false" >> $GITHUB_OUTPUT + fi + fi + - name: Check if version changed (for publish decision) + id: check_version + working-directory: ./rust/main + run: | + # Get current version from Cargo.toml workspace.package.version + CURRENT_VERSION=$(../scripts/ci/get-workspace-version.sh) + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "Current workspace version: $CURRENT_VERSION" + + # Get latest agents-v* tag + LATEST_TAG=$(../scripts/ci/get-latest-agents-tag.sh) + + if [ -z "$LATEST_TAG" ]; then + echo "latest_version=" >> $GITHUB_OUTPUT + echo "No previous release tag found, will create first release" + echo "should_release=true" >> $GITHUB_OUTPUT + else + LATEST_VERSION=$(echo "$LATEST_TAG" | sed 's/agents-v//') + echo "latest_version=$LATEST_VERSION" >> $GITHUB_OUTPUT + echo "Latest released version: $LATEST_VERSION" + + if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then + echo "Version has changed ($LATEST_VERSION -> $CURRENT_VERSION)" + echo "should_release=true" >> $GITHUB_OUTPUT + else + echo "Version unchanged" + echo "should_release=false" >> $GITHUB_OUTPUT + fi + fi + + release-pr: + name: Update Release PR + runs-on: depot-ubuntu-latest + needs: check-release-status + if: | + github.event_name == 'push' && + needs.check-release-status.outputs.has_changes == 'true' + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - name: Determine next version from conventional commits + id: next_version + env: + CURRENT_VERSION: ${{ needs.check-release-status.outputs.current_version }} + run: | + # Use helper script to determine next version + OUTPUT=$(./rust/scripts/ci/determine-next-version.sh "$CURRENT_VERSION") + NEW_VERSION=$(echo "$OUTPUT" | sed -n '1p') + BUMP_TYPE=$(echo "$OUTPUT" | sed -n '2p') + + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT + echo "Next version: $NEW_VERSION ($BUMP_TYPE bump from $CURRENT_VERSION)" + - name: Generate changelog + id: changelog + env: + NEW_VERSION: ${{ steps.next_version.outputs.new_version }} + run: | + # Get commit range for changelog generation + LATEST_TAG=$(./rust/scripts/ci/get-latest-agents-tag.sh) + + if [ -z "$LATEST_TAG" ]; then + COMMIT_RANGE="" + else + COMMIT_RANGE="${LATEST_TAG}..HEAD" + fi + + # Generate unified changelog for PR body + if [ -z "$COMMIT_RANGE" ]; then + CHANGELOG=$(./rust/scripts/ci/generate-workspace-changelog.sh --no-header) + else + CHANGELOG=$(./rust/scripts/ci/generate-workspace-changelog.sh "$COMMIT_RANGE" --no-header) + fi + + # Save changelog to file for PR body + echo "$CHANGELOG" > /tmp/changelog.md + + # Also output for GitHub Actions + { + echo 'changelog<> $GITHUB_OUTPUT + + # Generate per-workspace CHANGELOG.md files + ./rust/scripts/ci/generate-workspace-changelog.sh "$COMMIT_RANGE" "" --write-to-workspace "$NEW_VERSION" + - name: Update version files + env: + NEW_VERSION: ${{ steps.next_version.outputs.new_version }} + run: | + # Update workspace version in Cargo.toml + ./rust/scripts/ci/update-workspace-version.sh "$NEW_VERSION" + + # Update Cargo.lock in rust/main + cd rust/main + cargo update --workspace --offline 2>/dev/null || cargo update --workspace + echo "Updated rust/main/Cargo.lock" + + # Update Cargo.lock in rust/sealevel + cd ../sealevel + cargo update --workspace --offline 2>/dev/null || cargo update --workspace + echo "Updated rust/sealevel/Cargo.lock" + - name: Create or update release PR + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NEW_VERSION: ${{ steps.next_version.outputs.new_version }} + BUMP_TYPE: ${{ steps.next_version.outputs.bump_type }} + CHANGELOG: ${{ steps.changelog.outputs.changelog }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + BRANCH_NAME="release-agents-v${NEW_VERSION}" + + # Create branch from current HEAD (which is main) + git checkout -B "$BRANCH_NAME" + + # Stage changes (Cargo files and all workspace CHANGELOG.md files) + git add rust/main/Cargo.toml rust/main/Cargo.lock rust/sealevel/Cargo.lock + git add rust/main/*/CHANGELOG.md rust/main/*/*/CHANGELOG.md 2>/dev/null || true + + # Commit changes + git commit -m "release: agents v${NEW_VERSION} + + This is a $BUMP_TYPE version bump for the Hyperlane agents. + + Changes will be released as agents-v${NEW_VERSION} after this PR is merged." + + # Force push to obliterate any existing branch + git push -f origin "$BRANCH_NAME" + + # Create or update PR + PR_BODY="# Release agents v${NEW_VERSION} + + This PR prepares the release of Hyperlane agents version **${NEW_VERSION}** (${BUMP_TYPE} bump). + + ## What's Changed + + ${CHANGELOG} + + --- + + Once this PR is merged, the [\`rust-release.yml\`](https://github.com/${{ github.repository }}/blob/main/.github/workflows/rust-release.yml) workflow will automatically: + - Create a GitHub release with tag \`agents-v${NEW_VERSION}\` + - Trigger the build of release binaries + + 🤖 This PR was automatically created by the release workflow." + + # Check if PR already exists + EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number' 2>/dev/null || echo "") + + if [ -n "$EXISTING_PR" ]; then + echo "Updating existing PR #$EXISTING_PR" + gh pr edit "$EXISTING_PR" \ + --title "release: agents v${NEW_VERSION}" \ + --body "$PR_BODY" + echo "Updated PR: $(gh pr view $EXISTING_PR --json url --jq .url)" + else + echo "Creating new draft PR" + gh pr create \ + --title "release: agents v${NEW_VERSION}" \ + --body "$PR_BODY" \ + --base main \ + --head "$BRANCH_NAME" \ + --label "release" \ + --draft + PR_URL=$(gh pr list --head "$BRANCH_NAME" --json url --jq '.[0].url') + echo "Created draft PR: $PR_URL" + fi + - name: Summary + if: always() + env: + NEW_VERSION: ${{ steps.next_version.outputs.new_version }} + run: | + echo "### Release PR for agents v${NEW_VERSION}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The release PR has been created/updated." >> $GITHUB_STEP_SUMMARY + echo "Once merged, the release will be published automatically." >> $GITHUB_STEP_SUMMARY + + publish: + name: Publish Release + runs-on: depot-ubuntu-latest + needs: check-release-status + if: | + github.ref == 'refs/heads/main' && + (github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && needs.check-release-status.outputs.should_release == 'true')) + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Determine version and create release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IS_PRERELEASE: ${{ github.event_name == 'workflow_dispatch' }} + PRERELEASE_SUFFIX: ${{ inputs.prerelease_suffix }} + BASE_VERSION: ${{ needs.check-release-status.outputs.current_version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Determine final version based on release type + # workflow_dispatch always creates pre-releases + if [ "$IS_PRERELEASE" = "true" ]; then + # Pre-release: append suffix + if [ -n "$PRERELEASE_SUFFIX" ]; then + SUFFIX="$PRERELEASE_SUFFIX" + else + # Auto-generate preview.N + LAST_PREVIEW=$(git tag -l "agents-v${BASE_VERSION}-preview.*" | sort -V | tail -1) + if [ -z "$LAST_PREVIEW" ]; then + SUFFIX="preview.1" + else + PREVIEW_NUM=$(echo "$LAST_PREVIEW" | sed 's/.*preview\.\([0-9]*\)/\1/') + SUFFIX="preview.$((PREVIEW_NUM + 1))" + fi + fi + VERSION="${BASE_VERSION}-${SUFFIX}" + TITLE="Agents $VERSION (Pre-release)" + PRERELEASE_FLAG="--prerelease" + RELEASE_TYPE="Pre-release" + else + # Stable release + VERSION="$BASE_VERSION" + TITLE="Agents $VERSION" + PRERELEASE_FLAG="" + RELEASE_TYPE="Release" + fi + + TAG_NAME="agents-v${VERSION}" + echo "Creating $RELEASE_TYPE: $TAG_NAME" + + # Check if tag already exists + if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then + echo "Error: Tag $TAG_NAME already exists!" >&2 + echo "Cannot overwrite existing tag. Please use a different version or prerelease suffix." >&2 + exit 1 + fi + + # Check if GitHub release already exists + if gh release view "$TAG_NAME" --repo "${{ github.repository }}" >/dev/null 2>&1; then + echo "Error: GitHub release $TAG_NAME already exists!" >&2 + echo "Cannot overwrite existing release. Please use a different version or prerelease suffix." >&2 + exit 1 + fi + + # Generate workspace-grouped changelog + PREV_TAG=$(git describe --tags --abbrev=0 --match "agents-v*" 2>/dev/null || echo "") + + # For stable releases (push to main), use HEAD~1 to exclude the version bump commit + # For prereleases (workflow_dispatch), use HEAD since there's no version bump commit + if [ "$IS_PRERELEASE" = "true" ]; then + COMMIT_RANGE_END="HEAD" + else + COMMIT_RANGE_END="HEAD~1" + fi + + if [ -z "$PREV_TAG" ]; then + CHANGELOG=$(./rust/scripts/ci/generate-workspace-changelog.sh) + else + CHANGELOG=$(./rust/scripts/ci/generate-workspace-changelog.sh "${PREV_TAG}..${COMMIT_RANGE_END}") + fi + + # Generate new contributors section using GitHub's auto-generated notes + NEW_CONTRIBUTORS="" + if [ -n "$PREV_TAG" ]; then + # Use GitHub API to generate release notes and extract new contributors + AUTO_NOTES=$(gh api --method POST "/repos/${{ github.repository }}/releases/generate-notes" \ + -f tag_name="$TAG_NAME" \ + -f target_commitish="$COMMIT_RANGE_END" \ + -f previous_tag_name="$PREV_TAG" \ + --jq '.body' 2>/dev/null || echo "") + + # Extract the "New Contributors" section if it exists + if [ -n "$AUTO_NOTES" ]; then + NEW_CONTRIBUTORS=$(echo "$AUTO_NOTES" | sed -n '/^## New Contributors$/,/^## /p' | sed '$d') + # If we got the section, add proper spacing + if [ -n "$NEW_CONTRIBUTORS" ]; then + NEW_CONTRIBUTORS=$'\n\n'"${NEW_CONTRIBUTORS}" + fi + fi + fi + + # Add warning for pre-releases + if [ "$IS_PRERELEASE" = "true" ]; then + CHANGELOG="⚠️ **This is a pre-release version.**"$'\n\n'"${CHANGELOG}" + fi + + # Append new contributors section + CHANGELOG="${CHANGELOG}${NEW_CONTRIBUTORS}" + + # Create tag and GitHub release + git tag -a "$TAG_NAME" -m "$RELEASE_TYPE $TAG_NAME" + git push origin "$TAG_NAME" + + gh release create "$TAG_NAME" \ + --title "$TITLE" \ + --notes "$CHANGELOG" \ + $PRERELEASE_FLAG \ + --repo "${{ github.repository }}" + + echo "$RELEASE_TYPE $TAG_NAME published successfully!" >> $GITHUB_STEP_SUMMARY + [ "$IS_PRERELEASE" = "true" ] && echo "This is marked as a pre-release on GitHub." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Binary artifacts will be built automatically by the agent-release-artifacts workflow." >> $GITHUB_STEP_SUMMARY diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index 14f63bbce1..6f4194b5d4 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "abigen" -version = "0.1.0" +version = "1.5.0" dependencies = [ "Inflector", "cainome", @@ -2767,7 +2767,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto" -version = "0.1.0" +version = "1.5.0" dependencies = [ "elliptic-curve 0.13.8", "hex 0.4.3", @@ -4022,7 +4022,7 @@ dependencies = [ [[package]] name = "ethers-prometheus" -version = "0.1.0" +version = "1.5.0" dependencies = [ "abigen", "async-trait", @@ -5533,7 +5533,7 @@ dependencies = [ [[package]] name = "hyperlane-application" -version = "0.1.0" +version = "1.5.0" dependencies = [ "serde", "serde_json", @@ -5542,7 +5542,7 @@ dependencies = [ [[package]] name = "hyperlane-base" -version = "0.1.0" +version = "1.5.0" dependencies = [ "anyhow", "async-trait", @@ -5577,7 +5577,7 @@ dependencies = [ "hyperlane-sealevel", "hyperlane-starknet", "hyperlane-test", - "itertools 0.12.1", + "itertools 0.10.5", "maplit", "mockall", "moka", @@ -5610,7 +5610,7 @@ dependencies = [ [[package]] name = "hyperlane-core" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-rwlock", "async-trait", @@ -5633,7 +5633,7 @@ dependencies = [ "getrandom 0.2.15", "hex 0.4.3", "hyperlane-application", - "itertools 0.12.1", + "itertools 0.10.5", "num 0.4.3", "num-derive 0.4.2", "num-traits", @@ -5656,7 +5656,7 @@ dependencies = [ [[package]] name = "hyperlane-cosmos" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "base64 0.21.7", @@ -5664,7 +5664,7 @@ dependencies = [ "cometbft", "cometbft-rpc", "cosmrs", - "cosmwasm-std 2.1.3", + "cosmwasm-std 1.5.7", "crypto", "derive-new", "futures", @@ -5681,7 +5681,7 @@ dependencies = [ "ibc-proto", "injective-protobuf", "injective-std", - "itertools 0.12.1", + "itertools 0.10.5", "once_cell", "pin-project", "protobuf", @@ -5694,7 +5694,7 @@ dependencies = [ "time", "tokio", "tonic 0.12.3", - "tower 0.5.2", + "tower 0.4.13", "tracing", "tracing-futures", "url", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "hyperlane-ethereum" -version = "0.1.0" +version = "1.5.0" dependencies = [ "abigen", "async-trait", @@ -5756,7 +5756,7 @@ dependencies = [ "hyperlane-metric", "hyperlane-operation-verifier", "hyperlane-warp-route", - "itertools 0.12.1", + "itertools 0.10.5", "num 0.4.3", "num-traits", "reqwest 0.11.27", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "hyperlane-fuel" -version = "0.1.0" +version = "1.5.0" dependencies = [ "abigen", "anyhow", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "hyperlane-metric" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "derive-new", @@ -5803,7 +5803,7 @@ dependencies = [ [[package]] name = "hyperlane-operation-verifier" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "hyperlane-application", @@ -5995,7 +5995,7 @@ dependencies = [ [[package]] name = "hyperlane-starknet" -version = "0.1.0" +version = "1.5.0" dependencies = [ "abigen", "anyhow", @@ -6019,7 +6019,7 @@ dependencies = [ [[package]] name = "hyperlane-test" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "hyperlane-core", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "hyperlane-warp-route" -version = "0.1.0" +version = "1.5.0" dependencies = [ "hyperlane-core", ] @@ -6509,7 +6509,7 @@ dependencies = [ [[package]] name = "lander" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "chrono", @@ -6526,7 +6526,7 @@ dependencies = [ "hyperlane-ethereum", "hyperlane-radix", "hyperlane-sealevel", - "itertools 0.12.1", + "itertools 0.10.5", "mockall", "prometheus", "radix-common", @@ -6848,7 +6848,7 @@ dependencies = [ [[package]] name = "migration" -version = "0.1.0" +version = "1.5.0" dependencies = [ "sea-orm", "sea-orm-migration", @@ -8808,7 +8808,7 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "relayer" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "axum 0.8.4", @@ -8833,7 +8833,7 @@ dependencies = [ "hyperlane-metric", "hyperlane-operation-verifier", "hyperlane-test", - "itertools 0.12.1", + "itertools 0.10.5", "lander", "maplit", "mockall", @@ -8853,7 +8853,7 @@ dependencies = [ "tokio", "tokio-metrics", "tokio-test", - "tower 0.5.2", + "tower 0.4.13", "tracing", "tracing-futures", "tracing-subscriber", @@ -8966,7 +8966,7 @@ dependencies = [ [[package]] name = "reqwest-utils" -version = "0.1.0" +version = "1.5.0" dependencies = [ "reqwest 0.11.27", "thiserror 1.0.63", @@ -9176,7 +9176,7 @@ dependencies = [ [[package]] name = "run-locally" -version = "0.1.0" +version = "1.5.0" dependencies = [ "anyhow", "core-api-client", @@ -9685,7 +9685,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scraper" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "config", @@ -9699,7 +9699,7 @@ dependencies = [ "hyperlane-core", "hyperlane-ethereum", "hyperlane-test", - "itertools 0.12.1", + "itertools 0.10.5", "migration", "num-bigint 0.4.6", "num-traits", @@ -12944,7 +12944,7 @@ dependencies = [ [[package]] name = "validator" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-trait", "aws-config", @@ -12964,7 +12964,7 @@ dependencies = [ "hyperlane-cosmos", "hyperlane-ethereum", "hyperlane-test", - "itertools 0.12.1", + "itertools 0.10.5", "k256 0.13.4", "mockall", "prometheus", @@ -12976,7 +12976,7 @@ dependencies = [ "thiserror 1.0.63", "tokio", "tokio-test", - "tower 0.5.2", + "tower 0.4.13", "tracing", "tracing-futures", "tracing-test", diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 8bdc6b1e49..5c9c3f1dab 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -33,7 +33,7 @@ edition = "2021" homepage = "https://hyperlane.xyz" license-file = "../LICENSE.md" publish = false -version = "0.1.0" +version = "1.5.0" [workspace.dependencies] Inflector = "0.11.4" diff --git a/rust/main/hyperlane-metric/Cargo.toml b/rust/main/hyperlane-metric/Cargo.toml index 48c8f96407..c6ac11f588 100644 --- a/rust/main/hyperlane-metric/Cargo.toml +++ b/rust/main/hyperlane-metric/Cargo.toml @@ -15,4 +15,4 @@ maplit.workspace = true prometheus.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -url.workspace = true \ No newline at end of file +url.workspace = true diff --git a/rust/scripts/ci/determine-next-version.sh b/rust/scripts/ci/determine-next-version.sh new file mode 100755 index 0000000000..ce2c3ebdd3 --- /dev/null +++ b/rust/scripts/ci/determine-next-version.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Determine next version based on conventional commits +# +# Usage: +# determine-next-version.sh CURRENT_VERSION [COMMIT_RANGE] +# +# Arguments: +# CURRENT_VERSION - The current version (e.g., "1.4.0") +# COMMIT_RANGE - Optional git commit range (e.g., "v1.4.0..HEAD"). If omitted, uses latest tag..HEAD +# +# Returns: +# Outputs two lines: +# 1. The next version (e.g., "1.5.0") +# 2. The bump type ("major", "minor", or "patch") +# +# Examples: +# ./determine-next-version.sh "1.4.0" +# ./determine-next-version.sh "1.4.0" "v1.4.0..HEAD" +# +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "Error: CURRENT_VERSION is required" >&2 + echo "Usage: $0 CURRENT_VERSION [COMMIT_RANGE]" >&2 + exit 1 +fi + +CURRENT_VERSION="$1" +COMMIT_RANGE="${2:-}" + +# Determine script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUST_MAIN_DIR="$SCRIPT_DIR/../../main" + +# If no commit range specified, use commits since latest tag +if [ -z "$COMMIT_RANGE" ]; then + LATEST_TAG=$("$SCRIPT_DIR/get-latest-agents-tag.sh") + + if [ -z "$LATEST_TAG" ]; then + # Get all commits in rust/main + cd "$RUST_MAIN_DIR" + COMMITS=$(git log --oneline --no-merges -- .) + else + cd "$RUST_MAIN_DIR" + COMMITS=$(git log "${LATEST_TAG}..HEAD" --oneline --no-merges -- .) + fi +else + cd "$RUST_MAIN_DIR" + COMMITS=$(git log "$COMMIT_RANGE" --oneline --no-merges -- .) +fi + +# Parse current version +IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" + +# Analyze commits for conventional commit types +HAS_BREAKING=false +HAS_MINOR=false +HAS_PATCH=false + +while IFS= read -r commit; do + # Skip empty lines + [ -z "$commit" ] && continue + + # Check for breaking changes via ! suffix + if echo "$commit" | grep -qE "^[a-f0-9]+ [a-z]+(\(.+\))?!:"; then + HAS_BREAKING=true + # Check for minor changes (new features, refactors, perf improvements, chores) + elif echo "$commit" | grep -qE "^[a-f0-9]+ (feat|refactor|perf|chore)(\(.+\))?:"; then + HAS_MINOR=true + # Check for patch changes (fixes, docs, tests, ci, style, build) + elif echo "$commit" | grep -qE "^[a-f0-9]+ (fix|docs|test|ci|style|build)(\(.+\))?:"; then + HAS_PATCH=true + fi + + # Check commit body for BREAKING CHANGE + COMMIT_HASH=$(echo "$commit" | cut -d' ' -f1) + if git show -s --format=%B "$COMMIT_HASH" | grep -q "BREAKING CHANGE:"; then + HAS_BREAKING=true + fi +done <<< "$COMMITS" + +# Determine version bump +if [ "$HAS_BREAKING" = true ]; then + MAJOR=$((MAJOR + 1)) + MINOR=0 + PATCH=0 + BUMP_TYPE="major" +elif [ "$HAS_MINOR" = true ]; then + MINOR=$((MINOR + 1)) + PATCH=0 + BUMP_TYPE="minor" +elif [ "$HAS_PATCH" = true ]; then + PATCH=$((PATCH + 1)) + BUMP_TYPE="patch" +else + # Default to patch for any other changes + PATCH=$((PATCH + 1)) + BUMP_TYPE="patch" +fi + +NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}" + +# Output results (two lines) +echo "$NEW_VERSION" +echo "$BUMP_TYPE" diff --git a/rust/scripts/ci/generate-workspace-changelog.sh b/rust/scripts/ci/generate-workspace-changelog.sh new file mode 100755 index 0000000000..bb7c3b9bd1 --- /dev/null +++ b/rust/scripts/ci/generate-workspace-changelog.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env bash +# Script to generate workspace-grouped changelog for rust agents +# +# Usage: +# generate-workspace-changelog.sh [COMMIT_RANGE] [WORKSPACE_FILTER] [FLAGS] +# +# Arguments: +# COMMIT_RANGE - Git commit range (e.g., "v1.4.0..HEAD"). Defaults to latest tag..HEAD +# WORKSPACE_FILTER - Optional comma-separated list of workspaces to include (e.g., "agents/relayer,agents/validator") +# If omitted, all workspaces are included +# +# Flags: +# --no-header - Omit the "## What's Changed" header (useful for composing changelogs) +# --write-to-workspace VERSION - Update CHANGELOG.md files in each workspace directory +# +# Examples: +# ./generate-workspace-changelog.sh # All workspaces, latest tag..HEAD +# ./generate-workspace-changelog.sh "v1.4.0..v1.5.0" # All workspaces, specific range +# ./generate-workspace-changelog.sh "v1.4.0..v1.5.0" "agents/relayer" # Single workspace +# ./generate-workspace-changelog.sh "v1.4.0..v1.5.0" "agents/relayer,agents/validator" # Multiple workspaces +# ./generate-workspace-changelog.sh "v1.4.0..v1.5.0" "agents/relayer" --no-header # No header +# ./generate-workspace-changelog.sh "v1.4.0..v1.5.0" "" --write-to-workspace "1.5.0" # Update workspace CHANGELOGs +# +set -euo pipefail + +# Determine script directory and repo structure +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUST_MAIN_DIR="$SCRIPT_DIR/../../main" + +# Parse arguments +COMMIT_RANGE="" +WORKSPACE_FILTER="" +SHOW_HEADER=true +UPDATE_CHANGELOGS=false +VERSION="" + +# Parse all arguments +i=1 +while [ $i -le $# ]; do + arg="${!i}" + + if [ "$arg" = "--no-header" ]; then + SHOW_HEADER=false + elif [ "$arg" = "--write-to-workspace" ]; then + UPDATE_CHANGELOGS=true + # Get next argument as version + i=$((i + 1)) + if [ $i -le $# ]; then + VERSION="${!i}" + else + echo "Error: --write-to-workspace requires a VERSION argument" >&2 + exit 1 + fi + elif [ -z "$COMMIT_RANGE" ]; then + COMMIT_RANGE="$arg" + elif [ -z "$WORKSPACE_FILTER" ]; then + WORKSPACE_FILTER="$arg" + fi + + i=$((i + 1)) +done + +# Get the commit range +if [ -z "$COMMIT_RANGE" ]; then + # No commit range specified - use unreleased commits + LATEST_TAG=$(git tag -l "agents-v*" --sort=-version:refname | grep -E "^agents-v[0-9]+\.[0-9]+\.[0-9]+$" | head -1 || echo "") + if [ -z "$LATEST_TAG" ]; then + COMMIT_RANGE="HEAD" + else + COMMIT_RANGE="${LATEST_TAG}..HEAD" + fi +fi + +# Parse workspace filter into array +if [ -n "$WORKSPACE_FILTER" ]; then + IFS=',' read -ra FILTER_ARRAY <<< "$WORKSPACE_FILTER" +else + FILTER_ARRAY=() +fi + +# Temporary directory for categorization +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + +# Extract workspace members from rust/main/Cargo.toml +WORKSPACE_MEMBERS=$(grep -A 100 '^\[workspace\]' "$RUST_MAIN_DIR/Cargo.toml" | sed -n '/^members = \[/,/^\]/p' | grep '"' | sed 's/[", ]//g') + +# Helper function to check if a workspace should be included +should_include_workspace() { + local workspace="$1" + + # If no filter specified, include all + if [ ${#FILTER_ARRAY[@]} -eq 0 ]; then + return 0 + fi + + # Check if workspace is in filter list + for filter in "${FILTER_ARRAY[@]}"; do + if [ "$workspace" = "$filter" ]; then + return 0 + fi + done + + return 1 +} + +# Get all commits in the range (filter to rust/main directory) +git log --no-merges --format="%H" $COMMIT_RANGE -- rust/main | while read -r commit_hash; do + # Get commit message + commit_msg=$(git log -1 --format="%s" "$commit_hash") + + # Get files changed in this commit (within rust/main) + files=$(git diff-tree --no-commit-id --name-only -r "$commit_hash" -- rust/main) + + # Categorize based on workspace membership + workspace="" + for file in $files; do + # Strip rust/main/ prefix if present + file=$(echo "$file" | sed 's|^rust/main/||') + + # Check which workspace this file belongs to + for member in $WORKSPACE_MEMBERS; do + if [[ "$file" =~ ^"$member"(/|$) ]]; then + workspace="$member" + break 2 # Break both loops + fi + done + done + + # Default to "other" if no workspace found + if [ -z "$workspace" ]; then + workspace="other" + fi + + # Sanitize workspace name for file system (replace / with __) + workspace_file=$(echo "$workspace" | tr '/' '_') + + # Store commit in workspace category file (just message, PR# already in message) + echo "$commit_msg" >> "$TEMP_DIR/$workspace_file" +done + +# Function to generate changelog for a specific workspace +generate_workspace_changelog() { + local workspace="$1" + local include_header="${2:-true}" # Default to including header + local workspace_file=$(echo "$workspace" | tr '/' '_') + + if [ -f "$TEMP_DIR/$workspace_file" ]; then + if [ "$include_header" = "true" ]; then + echo "### $workspace" + echo "" + fi + sort -u "$TEMP_DIR/$workspace_file" | while read -r msg; do + echo "* $msg" + done + fi +} + +# If updating workspace changelogs, update files and exit +if [ "$UPDATE_CHANGELOGS" = true ]; then + if [ -z "$VERSION" ]; then + echo "Error: VERSION is required for --write-to-workspace" >&2 + exit 1 + fi + + echo "Updating workspace CHANGELOG.md files for version $VERSION..." + UPDATED_COUNT=0 + + for workspace in $WORKSPACE_MEMBERS; do + # Skip if workspace is filtered out + if ! should_include_workspace "$workspace"; then + continue + fi + + workspace_file=$(echo "$workspace" | tr '/' '_') + + # Skip if no changes for this workspace + if [ ! -f "$TEMP_DIR/$workspace_file" ]; then + continue + fi + + # Generate changelog content for this workspace (no header) + WORKSPACE_CHANGELOG=$(generate_workspace_changelog "$workspace" false) + + if [ -z "$WORKSPACE_CHANGELOG" ]; then + continue + fi + + WORKSPACE_CHANGELOG_FILE="$RUST_MAIN_DIR/$workspace/CHANGELOG.md" + WORKSPACE_DIR=$(dirname "$WORKSPACE_CHANGELOG_FILE") + + # Ensure directory exists + mkdir -p "$WORKSPACE_DIR" + + # Read existing changelog if it exists + if [ -f "$WORKSPACE_CHANGELOG_FILE" ]; then + CURRENT_WORKSPACE_CHANGELOG=$(cat "$WORKSPACE_CHANGELOG_FILE") + else + CURRENT_WORKSPACE_CHANGELOG="" + fi + + # Prepend new version to workspace changelog + { + echo "## [$VERSION] - $(date +%Y-%m-%d)" + echo "" + echo "$WORKSPACE_CHANGELOG" + if [ -n "$CURRENT_WORKSPACE_CHANGELOG" ]; then + echo "" + echo "$CURRENT_WORKSPACE_CHANGELOG" + fi + } > "$WORKSPACE_CHANGELOG_FILE" + + echo "Updated $workspace/CHANGELOG.md" + UPDATED_COUNT=$((UPDATED_COUNT + 1)) + done + + echo "Updated $UPDATED_COUNT workspace changelog(s)" + exit 0 +fi + +# Generate output (for display/PR body) +if [ "$SHOW_HEADER" = true ]; then + echo "## What's Changed" + echo "" +fi + +# Process workspace members in the order they appear in Cargo.toml, then "other" +FIRST_WORKSPACE=true +for workspace in $WORKSPACE_MEMBERS "other"; do + # Skip if workspace is filtered out + if ! should_include_workspace "$workspace"; then + continue + fi + + workspace_file=$(echo "$workspace" | tr '/' '_') + + if [ -f "$TEMP_DIR/$workspace_file" ]; then + # Add separator between workspaces (except before first one) + if [ "$FIRST_WORKSPACE" = false ]; then + echo "" + fi + FIRST_WORKSPACE=false + + generate_workspace_changelog "$workspace" + fi +done + +if [ "$SHOW_HEADER" = true ]; then + echo "" + echo "" +fi diff --git a/rust/scripts/ci/get-latest-agents-tag.sh b/rust/scripts/ci/get-latest-agents-tag.sh new file mode 100755 index 0000000000..a163bb52da --- /dev/null +++ b/rust/scripts/ci/get-latest-agents-tag.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# Get the latest stable agents-v* tag (excluding prereleases) +# +# Usage: +# get-latest-agents-tag.sh +# +# Returns: +# The latest agents-v* tag (e.g., "agents-v1.4.0"), or empty string if none found +# +set -euo pipefail + +git tag -l "agents-v*" --sort=-version:refname | grep -E "^agents-v[0-9]+\.[0-9]+\.[0-9]+$" | head -1 || true diff --git a/rust/scripts/ci/get-workspace-version.sh b/rust/scripts/ci/get-workspace-version.sh new file mode 100755 index 0000000000..4c728ef11c --- /dev/null +++ b/rust/scripts/ci/get-workspace-version.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Extract the version from rust/main/Cargo.toml workspace.package.version +# +# Usage: +# get-workspace-version.sh +# +# Returns: +# The current workspace version (e.g., "1.4.0") +# +set -euo pipefail + +# Determine script directory and repo structure +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUST_MAIN_DIR="$SCRIPT_DIR/../../main" + +grep -A 10 '^\[workspace\.package\]' "$RUST_MAIN_DIR/Cargo.toml" | \ + grep '^version = ' | \ + head -1 | \ + sed 's/version = "\(.*\)"/\1/' diff --git a/rust/scripts/ci/update-workspace-version.sh b/rust/scripts/ci/update-workspace-version.sh new file mode 100755 index 0000000000..b2c964ef06 --- /dev/null +++ b/rust/scripts/ci/update-workspace-version.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Update the workspace version in rust/main/Cargo.toml +# +# Usage: +# update-workspace-version.sh NEW_VERSION +# +# Arguments: +# NEW_VERSION - The new version to set (e.g., "1.5.0") +# +# Returns: +# Updates rust/main/Cargo.toml in place +# +set -euo pipefail + +if [ $# -ne 1 ]; then + echo "Error: NEW_VERSION is required" >&2 + echo "Usage: $0 NEW_VERSION" >&2 + exit 1 +fi + +NEW_VERSION="$1" + +# Determine script directory and repo structure +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +RUST_MAIN_DIR="$SCRIPT_DIR/../../main" +CARGO_TOML="$RUST_MAIN_DIR/Cargo.toml" + +# Use awk to find [workspace.package] section and update version within it +awk -v new_version="$NEW_VERSION" ' + /^\[workspace\.package\]/ { in_workspace=1 } + /^\[/ && !/^\[workspace\.package\]/ { in_workspace=0 } + in_workspace && /^version = / { + print "version = \"" new_version "\"" + next + } + { print } +' "$CARGO_TOML" > "$CARGO_TOML.new" + +mv "$CARGO_TOML.new" "$CARGO_TOML" + +echo "Updated $CARGO_TOML to version $NEW_VERSION" diff --git a/rust/sealevel/Cargo.lock b/rust/sealevel/Cargo.lock index 7ab7997794..651be71887 100644 --- a/rust/sealevel/Cargo.lock +++ b/rust/sealevel/Cargo.lock @@ -2401,7 +2401,7 @@ dependencies = [ [[package]] name = "hyperlane-application" -version = "0.1.0" +version = "1.5.0" dependencies = [ "serde", "serde_json", @@ -2410,7 +2410,7 @@ dependencies = [ [[package]] name = "hyperlane-core" -version = "0.1.0" +version = "1.5.0" dependencies = [ "async-rwlock", "async-trait", @@ -2428,7 +2428,7 @@ dependencies = [ "getrandom 0.2.15", "hex", "hyperlane-application", - "itertools 0.13.0", + "itertools 0.10.5", "num 0.4.3", "num-derive 0.4.2", "num-traits", @@ -2828,7 +2828,7 @@ dependencies = [ [[package]] name = "hyperlane-warp-route" -version = "0.1.0" +version = "1.5.0" dependencies = [ "hyperlane-core", ]