Skip to content

chore: update release tag to 26.02_18 #84

chore: update release tag to 26.02_18

chore: update release tag to 26.02_18 #84

Workflow file for this run

# 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