Release #9
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: 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" |