Merge pull request #24 from link-foundation/issue-19-d88ab983ff7d #21
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Rust CI/CD | |
| on: | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - 'rust/**' | |
| - '.github/workflows/rust.yml' | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| paths: | |
| - 'rust/**' | |
| - '.github/workflows/rust.yml' | |
| workflow_dispatch: | |
| inputs: | |
| release_mode: | |
| description: 'Manual release mode' | |
| required: true | |
| type: choice | |
| default: 'instant' | |
| options: | |
| - instant | |
| - changelog-pr | |
| bump_type: | |
| description: 'Version bump type' | |
| required: true | |
| type: choice | |
| options: | |
| - patch | |
| - minor | |
| - major | |
| description: | |
| description: 'Release description (optional)' | |
| required: false | |
| type: string | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| RUSTFLAGS: -Dwarnings | |
| jobs: | |
| # === DETECT CHANGES - determines which jobs should run === | |
| detect-changes: | |
| name: Detect Changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'workflow_dispatch' | |
| outputs: | |
| rs-changed: ${{ steps.changes.outputs.rs-changed }} | |
| toml-changed: ${{ steps.changes.outputs.toml-changed }} | |
| mjs-changed: ${{ steps.changes.outputs.mjs-changed }} | |
| docs-changed: ${{ steps.changes.outputs.docs-changed }} | |
| workflow-changed: ${{ steps.changes.outputs.workflow-changed }} | |
| any-code-changed: ${{ steps.changes.outputs.any-code-changed }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Detect changes | |
| id: changes | |
| working-directory: ./rust | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| GITHUB_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: node scripts/detect-code-changes.mjs | |
| # === CHANGELOG CHECK - only runs on PRs with code changes === | |
| # Docs-only PRs (./docs folder, markdown files) don't require changelog fragments | |
| changelog: | |
| name: Changelog Fragment Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Check for changelog fragments | |
| working-directory: ./rust | |
| env: | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| run: node scripts/check-changelog-fragment.mjs | |
| # === VERSION CHECK - prevents manual version modification in PRs === | |
| # This ensures versions are only modified by the automated release pipeline | |
| version-check: | |
| name: Version Modification Check | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Check for manual version changes | |
| working-directory: ./rust | |
| env: | |
| GITHUB_EVENT_NAME: ${{ github.event_name }} | |
| GITHUB_HEAD_REF: ${{ github.head_ref }} | |
| GITHUB_BASE_REF: ${{ github.base_ref }} | |
| run: node scripts/check-version-modification.mjs | |
| # === LINT AND FORMAT CHECK === | |
| # Lint runs independently of changelog check - it's a fast check that should always run | |
| lint: | |
| name: Lint and Format Check | |
| runs-on: ubuntu-latest | |
| needs: [detect-changes] | |
| # Note: always() is required because detect-changes is skipped on workflow_dispatch | |
| if: | | |
| always() && !cancelled() && ( | |
| github.event_name == 'push' || | |
| github.event_name == 'workflow_dispatch' || | |
| needs.detect-changes.outputs.rs-changed == 'true' || | |
| needs.detect-changes.outputs.toml-changed == 'true' || | |
| needs.detect-changes.outputs.mjs-changed == 'true' || | |
| needs.detect-changes.outputs.docs-changed == 'true' || | |
| needs.detect-changes.outputs.workflow-changed == 'true' | |
| ) | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Cache cargo dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/bin/ | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| rust/target/ | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} | |
| - name: Check formatting | |
| working-directory: ./rust | |
| run: cargo fmt --check | |
| - name: Run clippy | |
| working-directory: ./rust | |
| run: cargo clippy --all-targets --all-features | |
| - name: Check file size limit | |
| working-directory: ./rust | |
| run: node scripts/check-file-size.mjs | |
| # === TEST === | |
| # Test runs independently of changelog check | |
| test: | |
| name: Test (Rust on ${{ matrix.os }}) | |
| runs-on: ${{ matrix.os }} | |
| needs: [detect-changes, changelog] | |
| # Run if: push event, OR changelog succeeded, OR changelog was skipped (docs-only PR) | |
| if: always() && !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/bin/ | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| rust/target/ | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} | |
| - name: Run tests | |
| working-directory: ./rust | |
| run: cargo test --verbose | |
| - name: Run doc tests | |
| working-directory: ./rust | |
| run: cargo test --doc --verbose | |
| - name: Run example | |
| working-directory: ./rust | |
| run: cargo run --example basic_usage | |
| # === BUILD === | |
| # Build package - only runs if lint and test pass | |
| build: | |
| name: Build Package | |
| runs-on: ubuntu-latest | |
| needs: [lint, test] | |
| if: always() && !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Cache cargo dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/bin/ | |
| ~/.cargo/registry/index/ | |
| ~/.cargo/registry/cache/ | |
| ~/.cargo/git/db/ | |
| rust/target/ | |
| key: ${{ runner.os }}-cargo-build-${{ hashFiles('rust/Cargo.lock') }} | |
| - name: Build release | |
| working-directory: ./rust | |
| run: cargo build --release | |
| - name: Package crate | |
| working-directory: ./rust | |
| run: cargo package --list | |
| # === AUTO RELEASE === | |
| # Automatic release on push to main using changelog fragments | |
| auto-release: | |
| name: Auto Release | |
| needs: [lint, test, build] | |
| if: | | |
| always() && !cancelled() && | |
| github.event_name == 'push' && | |
| github.ref == 'refs/heads/main' && | |
| needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Configure git | |
| working-directory: ./rust | |
| run: node scripts/git-config.mjs | |
| - name: Determine bump type from changelog fragments | |
| id: bump_type | |
| working-directory: ./rust | |
| run: node scripts/get-bump-type.mjs | |
| - name: Check if version already released | |
| id: version_check | |
| working-directory: ./rust | |
| run: | | |
| # Get current version from Cargo.toml | |
| CURRENT_VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) | |
| echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| # Check if tag exists | |
| if git rev-parse "rust-v$CURRENT_VERSION" >/dev/null 2>&1; then | |
| echo "Tag rust-v$CURRENT_VERSION already exists" | |
| echo "should_release=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "New version detected: $CURRENT_VERSION" | |
| echo "should_release=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Collect changelog and bump version | |
| id: version | |
| if: steps.version_check.outputs.should_release == 'true' && steps.bump_type.outputs.has_fragments == 'true' | |
| working-directory: ./rust | |
| run: | | |
| node scripts/version-and-commit.mjs \ | |
| --bump-type "${{ steps.bump_type.outputs.bump_type }}" | |
| - name: Get current version | |
| id: current_version | |
| if: steps.version_check.outputs.should_release == 'true' | |
| working-directory: ./rust | |
| run: | | |
| VERSION=$(grep -Po '(?<=^version = ")[^"]*' Cargo.toml) | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: Build release | |
| if: steps.version_check.outputs.should_release == 'true' | |
| working-directory: ./rust | |
| run: cargo build --release | |
| - name: Publish to crates.io | |
| if: steps.version_check.outputs.should_release == 'true' | |
| working-directory: ./rust | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} | |
| run: | | |
| if [ -z "$CARGO_REGISTRY_TOKEN" ]; then | |
| echo "::warning::Neither CARGO_REGISTRY_TOKEN nor CARGO_TOKEN is set, skipping publish to crates.io" | |
| else | |
| cargo publish | |
| fi | |
| - name: Create GitHub Release | |
| if: steps.version_check.outputs.should_release == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ./rust | |
| run: | | |
| node scripts/create-github-release.mjs \ | |
| --version "${{ steps.current_version.outputs.version }}" \ | |
| --repository "${{ github.repository }}" \ | |
| --tag-prefix "rust-v" | |
| # === MANUAL INSTANT RELEASE === | |
| # Manual release via workflow_dispatch - only after CI passes | |
| manual-release: | |
| name: Instant Release | |
| needs: [lint, test, build] | |
| if: | | |
| always() && !cancelled() && | |
| github.event_name == 'workflow_dispatch' && | |
| github.event.inputs.release_mode == 'instant' && | |
| needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Configure git | |
| working-directory: ./rust | |
| run: node scripts/git-config.mjs | |
| - name: Collect changelog fragments | |
| working-directory: ./rust | |
| run: node scripts/collect-changelog.mjs | |
| - name: Version and commit | |
| id: version | |
| working-directory: ./rust | |
| env: | |
| BUMP_TYPE: ${{ github.event.inputs.bump_type }} | |
| DESCRIPTION: ${{ github.event.inputs.description }} | |
| run: node scripts/version-and-commit.mjs --bump-type "${{ github.event.inputs.bump_type }}" --description "${{ github.event.inputs.description }}" | |
| - name: Build release | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| working-directory: ./rust | |
| run: cargo build --release | |
| - name: Publish to crates.io | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| working-directory: ./rust | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN || secrets.CARGO_TOKEN }} | |
| run: | | |
| if [ -z "$CARGO_REGISTRY_TOKEN" ]; then | |
| echo "::warning::Neither CARGO_REGISTRY_TOKEN nor CARGO_TOKEN is set, skipping publish to crates.io" | |
| else | |
| cargo publish | |
| fi | |
| - name: Create GitHub Release | |
| if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| working-directory: ./rust | |
| run: | | |
| node scripts/create-github-release.mjs \ | |
| --version "${{ steps.version.outputs.new_version }}" \ | |
| --repository "${{ github.repository }}" \ | |
| --tag-prefix "rust-v" | |
| # === MANUAL CHANGELOG PR === | |
| changelog-pr: | |
| name: Create Changelog PR | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.release_mode == 'changelog-pr' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Create changelog fragment | |
| working-directory: ./rust | |
| run: | | |
| # Create timestamp for unique filename | |
| TIMESTAMP=$(date +"%Y%m%d_%H%M%S") | |
| FILENAME="changelog.d/${TIMESTAMP}_manual_release.md" | |
| # Create the changelog fragment | |
| cat > "$FILENAME" << EOF | |
| --- | |
| bump: ${{ github.event.inputs.bump_type }} | |
| --- | |
| ${{ github.event.inputs.description || 'Manual release' }} | |
| EOF | |
| echo "Created changelog fragment: $FILENAME" | |
| - name: Create Pull Request | |
| uses: peter-evans/create-pull-request@v7 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| commit-message: 'chore: add changelog for manual ${{ github.event.inputs.bump_type }} release' | |
| branch: changelog-manual-release-${{ github.run_id }} | |
| delete-branch: true | |
| title: 'chore: manual ${{ github.event.inputs.bump_type }} release (Rust)' | |
| body: | | |
| ## Manual Release Request (Rust) | |
| This PR was created by a manual workflow trigger to prepare a **${{ github.event.inputs.bump_type }}** release for the Rust package. | |
| ### Release Details | |
| - **Type:** ${{ github.event.inputs.bump_type }} | |
| - **Description:** ${{ github.event.inputs.description || 'Manual release' }} | |
| - **Triggered by:** @${{ github.actor }} | |
| ### Next Steps | |
| 1. Review the changelog fragment in this PR | |
| 2. Merge this PR to main | |
| 3. The automated release workflow will publish to crates.io and create a GitHub release |