chore: update release tag to 26.02_18 #84
Workflow file for this run
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
| # Release workflow - builds and publishes release artifacts | |
| # Triggered on new CalVer tags (YY.MM_PATCH format) | |
| # | |
| # CalVer format: YY.MM_PATCH | |
| # Examples: 24.12_0, 25.01_1, 25.06_12 | |
| # | |
| # Safety guarantees: | |
| # - Builds ALL targets BEFORE incrementing version or publishing | |
| # - Version bump and crates.io publish only happen after successful builds | |
| # - If any build fails, no version changes or publications occur | |
| # | |
| # This workflow: | |
| # 1. Validates the CalVer tag format | |
| # 2. Builds all platform binaries (validation phase) | |
| # 3. Builds Docker image (validation phase) | |
| # 4. Only after ALL builds succeed: | |
| # - Increments semver in Cargo.toml | |
| # - Publishes to crates.io | |
| # - Creates GitHub release with artifacts | |
| name: Release | |
| on: | |
| push: | |
| tags: | |
| - '[0-9][0-9].[0-9][0-9]_[0-9]*' | |
| # Allow manual trigger for testing | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Version to build (CalVer: YY.MM_PATCH)' | |
| required: true | |
| type: string | |
| dry-run: | |
| description: 'Dry run (skip crates.io publish and release creation)' | |
| required: false | |
| default: true | |
| type: boolean | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # ============================================================ | |
| # PHASE 1: Validation - Must pass before any publishing | |
| # ============================================================ | |
| # Validate version format | |
| validate: | |
| name: Validate Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| calver: ${{ steps.version.outputs.calver }} | |
| current-semver: ${{ steps.version.outputs.current_semver }} | |
| new-semver: ${{ steps.version.outputs.new_semver }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Get version info | |
| id: version | |
| run: | | |
| if [ "${{ github.event_name }}" = "push" ]; then | |
| CALVER="${{ github.ref_name }}" | |
| else | |
| CALVER="${{ inputs.version }}" | |
| fi | |
| # Validate CalVer format: YY.MM_PATCH (e.g., 24.12_0, 25.01_15) | |
| if ! echo "$CALVER" | grep -qE '^[0-9]{2}\.(0[1-9]|1[0-2])_[0-9]+$'; then | |
| echo "::error::Invalid CalVer format: $CALVER" | |
| echo "Expected format: YY.MM_PATCH (e.g., 24.12_0, 25.01_1)" | |
| exit 1 | |
| fi | |
| echo "calver=$CALVER" >> $GITHUB_OUTPUT | |
| echo "Valid CalVer: $CALVER" | |
| # Get current semver from Cargo.toml | |
| CURRENT_SEMVER=$(grep -m1 'version = "' Cargo.toml | sed 's/.*version = "\([^"]*\)".*/\1/') | |
| echo "current_semver=$CURRENT_SEMVER" >> $GITHUB_OUTPUT | |
| echo "Current semver: $CURRENT_SEMVER" | |
| # Calculate new semver (increment patch) | |
| MAJOR=$(echo $CURRENT_SEMVER | cut -d. -f1) | |
| MINOR=$(echo $CURRENT_SEMVER | cut -d. -f2) | |
| PATCH=$(echo $CURRENT_SEMVER | cut -d. -f3) | |
| NEW_PATCH=$((PATCH + 1)) | |
| NEW_SEMVER="${MAJOR}.${MINOR}.${NEW_PATCH}" | |
| echo "new_semver=$NEW_SEMVER" >> $GITHUB_OUTPUT | |
| echo "New semver will be: $NEW_SEMVER" | |
| # Build all platform binaries (validation - no publishing yet) | |
| build: | |
| name: Build | |
| needs: validate | |
| uses: ./.github/workflows/_build.yml | |
| with: | |
| profile: release | |
| upload-artifacts: true | |
| artifact-prefix: zentinel-${{ needs.validate.outputs.calver }} | |
| version: ${{ needs.validate.outputs.calver }} | |
| targets: | | |
| [ | |
| {"os": "ubuntu-latest", "target": "x86_64-unknown-linux-gnu", "suffix": "linux-amd64"}, | |
| {"os": "ubuntu-latest", "target": "aarch64-unknown-linux-gnu", "suffix": "linux-arm64"}, | |
| {"os": "macos-latest", "target": "x86_64-apple-darwin", "suffix": "darwin-amd64"}, | |
| {"os": "macos-latest", "target": "aarch64-apple-darwin", "suffix": "darwin-arm64"} | |
| ] | |
| # Build Docker image (validation - no pushing yet) | |
| docker-build: | |
| name: Docker Build | |
| needs: [validate, build] | |
| uses: ./.github/workflows/_docker.yml | |
| with: | |
| version: ${{ needs.validate.outputs.calver }} | |
| artifact-prefix: zentinel-${{ needs.validate.outputs.calver }} | |
| push: false # Don't push yet - just validate it builds | |
| latest: false | |
| permissions: | |
| contents: read | |
| packages: write # Required by nested workflow even when not pushing | |
| id-token: write # Required by _docker.yml (cosign steps gated on push) | |
| # Sign release binaries with cosign (keyless / Sigstore OIDC) | |
| sign-binaries: | |
| name: Sign Binaries | |
| needs: [validate, build] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write # Required for keyless signing (Sigstore OIDC) | |
| steps: | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@v3 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| path: artifacts | |
| - name: Create and sign release archives | |
| run: | | |
| VERSION="${{ needs.validate.outputs.calver }}" | |
| mkdir -p signed | |
| for dir in artifacts/zentinel-${VERSION}-*/; do | |
| if [ -d "$dir" ]; then | |
| SUFFIX=$(basename "$dir" | sed "s/zentinel-${VERSION}-//") | |
| # Skip non-platform artifacts (e.g., sbom) | |
| case "$SUFFIX" in | |
| linux-amd64|linux-arm64|darwin-amd64|darwin-arm64) ;; | |
| *) echo "Skipping non-platform artifact: $SUFFIX"; continue ;; | |
| esac | |
| ARCHIVE_NAME="zentinel-${VERSION}-${SUFFIX}.tar.gz" | |
| tar -czvf "signed/$ARCHIVE_NAME" -C "$dir" . | |
| cosign sign-blob --yes "signed/$ARCHIVE_NAME" \ | |
| --bundle "signed/${ARCHIVE_NAME}.bundle" | |
| fi | |
| done | |
| echo "Signed archives:" | |
| ls -la signed/ | |
| - name: Upload signed archives | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: signed-archives | |
| path: signed/ | |
| retention-days: 30 | |
| # ============================================================ | |
| # PHASE 2: Publishing - Only runs after ALL builds succeed | |
| # ============================================================ | |
| # Update version in Cargo.toml and publish to crates.io | |
| publish-crates: | |
| name: Publish to crates.io | |
| needs: [validate, build, docker-build] # Waits for ALL builds | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry-run == false) | |
| environment: release # Optional: use GitHub environment for additional protection | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| outputs: | |
| commit-sha: ${{ steps.commit.outputs.sha }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Update Cargo.toml versions | |
| run: | | |
| NEW_SEMVER="${{ needs.validate.outputs.new-semver }}" | |
| CURRENT_SEMVER="${{ needs.validate.outputs.current-semver }}" | |
| echo "Updating workspace version from $CURRENT_SEMVER to $NEW_SEMVER" | |
| # Update workspace version | |
| sed -i "s/^version = \"$CURRENT_SEMVER\"/version = \"$NEW_SEMVER\"/" Cargo.toml | |
| # Update internal dependency versions | |
| find crates -name "Cargo.toml" -exec sed -i "s/version = \"$CURRENT_SEMVER\"/version = \"$NEW_SEMVER\"/g" {} \; | |
| # Verify updates | |
| echo "Updated versions:" | |
| grep -r "version = \"$NEW_SEMVER\"" Cargo.toml crates/*/Cargo.toml || true | |
| - name: Commit version bump | |
| id: commit | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Cargo.toml crates/*/Cargo.toml | |
| git commit -m "chore: bump version to ${{ needs.validate.outputs.new-semver }} for release ${{ needs.validate.outputs.calver }}" | |
| echo "sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT | |
| - name: Push version bump to main | |
| id: push | |
| continue-on-error: true | |
| run: git push origin HEAD:main | |
| - name: Create PR for version bump if push was blocked | |
| if: steps.push.outcome == 'failure' | |
| continue-on-error: true # Don't block crates.io publish if PR creation fails | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| BRANCH="chore/bump-${{ needs.validate.outputs.new-semver }}" | |
| git checkout -b "$BRANCH" | |
| git push --force origin "$BRANCH" | |
| gh pr create \ | |
| --title "chore: bump version to ${{ needs.validate.outputs.new-semver }} for release ${{ needs.validate.outputs.calver }}" \ | |
| --body "Automated version bump after release ${{ needs.validate.outputs.calver }}. Direct push to main was blocked by branch protection." \ | |
| --base main | |
| - name: Setup Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Publish crates | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| run: | | |
| # Publish in dependency order with retry logic | |
| CRATES=( | |
| "zentinel-common" | |
| "zentinel-config" | |
| "zentinel-agent-protocol" | |
| "zentinel-proxy" | |
| ) | |
| NEW_SEMVER="${{ needs.validate.outputs.new-semver }}" | |
| for crate in "${CRATES[@]}"; do | |
| echo "Publishing $crate..." | |
| # Check if this version is already published (from a partial prior run) | |
| if cargo search "$crate" --limit 1 2>/dev/null | grep -q "\"$NEW_SEMVER\""; then | |
| echo "$crate@$NEW_SEMVER already published, skipping" | |
| continue | |
| fi | |
| # Retry up to 3 times (crates.io index propagation) | |
| for i in 1 2 3; do | |
| if cargo publish -p "$crate" --no-verify; then | |
| echo "Successfully published $crate" | |
| break | |
| else | |
| if [ $i -lt 3 ]; then | |
| echo "Publish failed, waiting 30s for index to update..." | |
| sleep 30 | |
| else | |
| echo "::error::Failed to publish $crate after 3 attempts" | |
| exit 1 | |
| fi | |
| fi | |
| done | |
| # Wait for crates.io index to update before next crate | |
| sleep 15 | |
| done | |
| # Push Docker image (only after crates published) | |
| docker-push: | |
| name: Docker Push | |
| needs: [validate, build, publish-crates] | |
| uses: ./.github/workflows/_docker.yml | |
| with: | |
| version: ${{ needs.validate.outputs.calver }} | |
| artifact-prefix: zentinel-${{ needs.validate.outputs.calver }} | |
| push: true | |
| latest: true | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # Required for cosign keyless signing | |
| # Create GitHub release with assets | |
| release: | |
| name: Create Release | |
| needs: [validate, build, sign-binaries, publish-crates] | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry-run == false) | |
| permissions: | |
| contents: write | |
| outputs: | |
| hashes: ${{ steps.hash.outputs.hashes }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Download signed archives | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: signed-archives | |
| path: signed | |
| - name: Download SBOM artifact | |
| uses: actions/download-artifact@v7 | |
| with: | |
| name: zentinel-${{ needs.validate.outputs.calver }}-sbom | |
| path: sbom | |
| - name: Prepare release assets | |
| run: | | |
| VERSION="${{ needs.validate.outputs.calver }}" | |
| mkdir -p release | |
| # Copy signed archives and bundles | |
| cp signed/*.tar.gz release/ | |
| cp signed/*.bundle release/ | |
| # Generate checksums | |
| cd release | |
| for archive in *.tar.gz; do | |
| sha256sum "$archive" > "${archive}.sha256" | |
| done | |
| cd .. | |
| # Copy SBOM files | |
| if [ -f "sbom/zentinel-sbom.cdx.json" ]; then | |
| cp "sbom/zentinel-sbom.cdx.json" "release/zentinel-${VERSION}-sbom.cdx.json" | |
| cp "sbom/zentinel-sbom.spdx.json" "release/zentinel-${VERSION}-sbom.spdx.json" | |
| fi | |
| echo "Release assets:" | |
| ls -la release/ | |
| - name: Generate base64-encoded hashes | |
| id: hash | |
| run: | | |
| cd release | |
| sha256sum *.tar.gz > checksums.txt | |
| echo "hashes=$(base64 -w0 checksums.txt)" >> $GITHUB_OUTPUT | |
| - name: Generate release notes | |
| run: | | |
| CALVER="${{ needs.validate.outputs.calver }}" | |
| SEMVER="${{ needs.validate.outputs.new-semver }}" | |
| # Get previous tag for changelog link | |
| PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") | |
| cat << 'HEREDOC' > release_notes.md | |
| ## Zentinel $CALVER | |
| **Cargo version:** `$SEMVER` | |
| ### Installation | |
| #### From crates.io | |
| ```bash | |
| cargo install zentinel-proxy | |
| ``` | |
| #### From binary | |
| Download the appropriate archive for your platform and extract: | |
| ```bash | |
| tar -xzf zentinel-$CALVER-linux-amd64.tar.gz | |
| sudo mv zentinel /usr/local/bin/ | |
| ``` | |
| #### Docker | |
| ```bash | |
| docker pull ghcr.io/zentinelproxy/zentinel:$CALVER | |
| ``` | |
| ### Supply Chain Security | |
| All release archives are signed with [Sigstore cosign](https://docs.sigstore.dev/) using keyless signing tied to GitHub Actions OIDC identity. SLSA v1.0 provenance is attached to this release. | |
| #### Verify a binary | |
| ```bash | |
| cosign verify-blob --bundle zentinel-$CALVER-linux-amd64.tar.gz.bundle \ | |
| --certificate-identity-regexp "github.com/zentinelproxy/zentinel" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ | |
| zentinel-$CALVER-linux-amd64.tar.gz | |
| ``` | |
| #### Verify the container image | |
| ```bash | |
| cosign verify ghcr.io/zentinelproxy/zentinel:$CALVER \ | |
| --certificate-identity-regexp "github.com/zentinelproxy/zentinel" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" | |
| ``` | |
| ### Checksums | |
| Verify downloads with the `.sha256` files. | |
| ### Software Bill of Materials | |
| CycloneDX 1.5 and SPDX 2.3 SBOMs are attached as release assets. | |
| HEREDOC | |
| # Replace variables | |
| sed -i "s/\$CALVER/$CALVER/g" release_notes.md | |
| sed -i "s/\$SEMVER/$SEMVER/g" release_notes.md | |
| # Add changelog link if we have a previous tag | |
| if [ -n "$PREV_TAG" ]; then | |
| echo "" >> release_notes.md | |
| echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${CALVER}" >> release_notes.md | |
| fi | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ needs.validate.outputs.calver }} | |
| name: Release ${{ needs.validate.outputs.calver }} | |
| body_path: release_notes.md | |
| files: release/* | |
| fail_on_unmatched_files: true | |
| # SLSA provenance for release artifacts | |
| provenance: | |
| name: SLSA Provenance | |
| needs: [release] | |
| if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.dry-run == false) | |
| permissions: | |
| actions: read | |
| id-token: write | |
| contents: write | |
| uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 | |
| with: | |
| base64-subjects: "${{ needs.release.outputs.hashes }}" | |
| upload-assets: true | |
| upload-tag-name: ${{ github.ref_name }} | |
| # ============================================================ | |
| # Summary | |
| # ============================================================ | |
| summary: | |
| name: Summary | |
| needs: [validate, build, docker-build, sign-binaries, publish-crates, docker-push, release, provenance] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Summary | |
| run: | | |
| echo "## Release Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Version | Value |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| CalVer | \`${{ needs.validate.outputs.calver }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Semver | \`${{ needs.validate.outputs.new-semver }}\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Phase 1: Validation" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Validate | ${{ needs.validate.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Build | ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Build | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Sign Binaries | ${{ needs.sign-binaries.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Phase 2: Publishing" >> $GITHUB_STEP_SUMMARY | |
| echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Publish Crates | ${{ needs.publish-crates.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker Push | ${{ needs.docker-push.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Release | ${{ needs.release.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| SLSA Provenance | ${{ needs.provenance.result }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| # Show failure message if validation phase failed | |
| if [ "${{ needs.build.result }}" = "failure" ] || [ "${{ needs.docker-build.result }}" = "failure" ]; then | |
| echo "### ⚠️ Build Failed" >> $GITHUB_STEP_SUMMARY | |
| echo "No version bump or publishing occurred because the build failed." >> $GITHUB_STEP_SUMMARY | |
| echo "Fix the build issues and push a new tag." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "### Artifacts" >> $GITHUB_STEP_SUMMARY | |
| echo "- **crates.io**: [zentinel-proxy](https://crates.io/crates/zentinel-proxy)" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Container**: \`ghcr.io/zentinelproxy/zentinel:${{ needs.validate.outputs.calver }}\`" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Release**: https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate.outputs.calver }}" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Supply Chain" >> $GITHUB_STEP_SUMMARY | |
| echo "- Binaries signed with Sigstore cosign (keyless)" >> $GITHUB_STEP_SUMMARY | |
| echo "- Container images signed with cosign" >> $GITHUB_STEP_SUMMARY | |
| echo "- SBOM attached (CycloneDX + SPDX)" >> $GITHUB_STEP_SUMMARY | |
| echo "- SLSA v1.0 provenance attested" >> $GITHUB_STEP_SUMMARY | |
| fi |