Skip to content

Release

Release #9

Workflow file for this run

name: Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., 0.2.0 - no v prefix)'
required: true
type: string
# Prevent concurrent releases
concurrency:
group: release
cancel-in-progress: false
# Restrict permissions to minimum required
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
validate:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.validate.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate version format
id: validate
run: |
VERSION="${{ inputs.version }}"
if ! echo "$VERSION" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "::error::Version must be in format X.Y.Z or X.Y.Z-suffix (e.g., 0.2.0, 1.0.0-alpha.1)"
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Valid version: $VERSION"
- name: Check tag does not exist
run: |
if git rev-parse "v${{ steps.validate.outputs.version }}" >/dev/null 2>&1; then
echo "::error::Tag v${{ steps.validate.outputs.version }} already exists"
exit 1
fi
echo "Tag v${{ steps.validate.outputs.version }} is available"
# Test gate: ensure code compiles and tests pass
test:
runs-on: macos-latest
needs: validate
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-test-
- name: Run tests
run: cargo test --all-features
- name: Build release binary
run: cargo build --release
# Security gate: must pass before release proceeds
security:
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
key: ${{ runner.os }}-cargo-security-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-security-
- name: Install security tools
run: |
command -v cargo-audit || cargo install cargo-audit --locked
command -v cargo-deny || cargo install cargo-deny --locked
- name: Run cargo audit
run: cargo audit --deny warnings
- name: Run cargo deny
run: cargo deny check
# Prepare release: bump version, generate changelog, create tag
prepare:
runs-on: ubuntu-latest
needs: [validate, test, security]
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: agentic-dev3o
repositories: sandbox-shell
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
key: ${{ runner.os }}-cargo-release-tools-v1
- name: Install release tools
run: |
# Install cargo-edit for version bumping
command -v cargo-set-version || cargo install cargo-edit --locked
# Install git-cliff for changelog generation
command -v git-cliff || cargo install git-cliff --locked
- name: Bump version in Cargo.toml
run: |
cargo set-version ${{ needs.validate.outputs.version }}
cargo update -p sx
- name: Generate changelog
run: |
git-cliff --tag v${{ needs.validate.outputs.version }} --output CHANGELOG.md
- name: Commit version bump and changelog
run: |
git config user.name "dev3o[bot]"
git config user.email "${{ vars.APP_ID }}+dev3o[bot]@users.noreply.github.com"
git add Cargo.toml Cargo.lock CHANGELOG.md
git commit -m "chore(release): v${{ needs.validate.outputs.version }}"
git tag -a "v${{ needs.validate.outputs.version }}" -m "Release v${{ needs.validate.outputs.version }}"
git push origin main
git push origin "v${{ needs.validate.outputs.version }}"
# Build release binaries for macOS (Intel and Apple Silicon)
build:
runs-on: macos-latest
needs: [validate, prepare]
strategy:
fail-fast: false
matrix:
target:
- x86_64-apple-darwin
- aarch64-apple-darwin
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: v${{ needs.validate.outputs.version }}
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-cargo-
- name: Build release binary
run: cargo build --release --target ${{ matrix.target }}
- name: Create archive
run: |
mkdir -p dist
cp target/${{ matrix.target }}/release/sx dist/
cd dist
tar czf sx-${{ needs.validate.outputs.version }}-${{ matrix.target }}.tar.gz sx
shasum -a 256 sx-${{ needs.validate.outputs.version }}-${{ matrix.target }}.tar.gz > sx-${{ needs.validate.outputs.version }}-${{ matrix.target }}.tar.gz.sha256
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sx-${{ matrix.target }}
path: dist/*.tar.gz*
retention-days: 1
# Create GitHub release with all artifacts
release:
runs-on: ubuntu-latest
needs: [validate, build]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: v${{ needs.validate.outputs.version }}
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: List artifacts
run: ls -la artifacts/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ needs.validate.outputs.version }}
name: v${{ needs.validate.outputs.version }}
body_path: CHANGELOG.md
draft: false
prerelease: ${{ contains(needs.validate.outputs.version, '-') }}
files: |
artifacts/*.tar.gz
artifacts/*.sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Update Homebrew tap with new version
# NOTE: This job is specific to agentic-dev3o organization.
# Forks should either disable this job or update the owner/repositories values.
update-tap:
runs-on: ubuntu-latest
needs: [validate, release]
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: v${{ needs.validate.outputs.version }}
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: agentic-dev3o
repositories: homebrew-sx
- name: Update Homebrew formula
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
set -e
VERSION="${{ needs.validate.outputs.version }}"
REPO="agentic-dev3o/sandbox-shell"
TAP_REPO="agentic-dev3o/homebrew-sx"
echo "==> Updating formula to version ${VERSION}"
# Download ARM64 binary and calculate SHA256
ARM64_URL="https://github.com/${REPO}/releases/download/v${VERSION}/sx-${VERSION}-aarch64-apple-darwin.tar.gz"
echo "==> Downloading ARM64 binary: ${ARM64_URL}"
HTTP_CODE=$(curl -sL -w "%{http_code}" -o /tmp/arm64.tar.gz "$ARM64_URL")
if [ "$HTTP_CODE" != "200" ]; then
echo "Error: Failed to download ARM64 binary (HTTP ${HTTP_CODE})"
exit 1
fi
ARM64_SHA256=$(shasum -a 256 /tmp/arm64.tar.gz | cut -d' ' -f1)
echo "==> ARM64 SHA256: ${ARM64_SHA256}"
# Download x86_64 binary and calculate SHA256
X86_64_URL="https://github.com/${REPO}/releases/download/v${VERSION}/sx-${VERSION}-x86_64-apple-darwin.tar.gz"
echo "==> Downloading x86_64 binary: ${X86_64_URL}"
HTTP_CODE=$(curl -sL -w "%{http_code}" -o /tmp/x86_64.tar.gz "$X86_64_URL")
if [ "$HTTP_CODE" != "200" ]; then
echo "Error: Failed to download x86_64 binary (HTTP ${HTTP_CODE})"
exit 1
fi
X86_64_SHA256=$(shasum -a 256 /tmp/x86_64.tar.gz | cut -d' ' -f1)
echo "==> x86_64 SHA256: ${X86_64_SHA256}"
# Get current formula
echo "==> Fetching current formula"
gh api "repos/${TAP_REPO}/contents/Formula/sx.rb" --jq '.content' | base64 -d > /tmp/formula.rb
# Update formula (version is auto-extracted from URL by Homebrew)
echo "==> Updating formula"
# Update ARM64 URL
sed -i "s|releases/download/v[^/]*/sx-[^-]*-aarch64|releases/download/v${VERSION}/sx-${VERSION}-aarch64|" /tmp/formula.rb
# Update x86_64 URL
sed -i "s|releases/download/v[^/]*/sx-[^-]*-x86_64|releases/download/v${VERSION}/sx-${VERSION}-x86_64|" /tmp/formula.rb
# Update SHA256 values - ARM64 first (appears first in file), then x86_64
# Use awk to update sha256 values in order of appearance
awk -v arm64="${ARM64_SHA256}" -v x86="${X86_64_SHA256}" '
/sha256 "[a-f0-9]+"/ {
if (!first_done) {
sub(/sha256 "[a-f0-9]+"/, "sha256 \"" arm64 "\"")
first_done = 1
} else {
sub(/sha256 "[a-f0-9]+"/, "sha256 \"" x86 "\"")
}
}
{ print }
' /tmp/formula.rb > /tmp/formula_updated.rb
mv /tmp/formula_updated.rb /tmp/formula.rb
# Show changes
echo "==> Updated formula:"
cat /tmp/formula.rb
# Get current file SHA (needed for update)
FILE_SHA=$(gh api "repos/${TAP_REPO}/contents/Formula/sx.rb" --jq '.sha')
echo "==> Current file SHA: ${FILE_SHA}"
# Commit updated formula
echo "==> Committing changes"
CONTENT_BASE64=$(base64 -w 0 /tmp/formula.rb)
gh api "repos/${TAP_REPO}/contents/Formula/sx.rb" \
-X PUT \
-f message="sx ${VERSION}" \
-f content="${CONTENT_BASE64}" \
-f sha="$FILE_SHA" \
-f branch="main"
echo "==> Formula updated successfully"