diff --git a/.github/workflows/QA.yaml b/.github/workflows/QA.yaml index 120a67f..da4416c 100644 --- a/.github/workflows/QA.yaml +++ b/.github/workflows/QA.yaml @@ -48,7 +48,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - name: Cache cargo-audit + uses: actions/cache@v4 + id: cache-audit + with: + path: ~/.cargo/bin/cargo-audit + key: cargo-audit-0.21 - name: Install cargo-audit + if: steps.cache-audit.outputs.cache-hit != 'true' run: cargo install cargo-audit --locked - name: Run security audit run: cargo audit --deny warnings @@ -70,7 +77,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - name: Cache cargo-deny + uses: actions/cache@v4 + id: cache-deny + with: + path: ~/.cargo/bin/cargo-deny + key: cargo-deny-0.18 - name: Install cargo-deny + if: steps.cache-deny.outputs.cache-hit != 'true' run: cargo install cargo-deny --locked - name: Check dependencies run: cargo deny check diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8826bf5 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,278 @@ +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: | + cargo install cargo-audit --locked + 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: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_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 + cargo install cargo-edit --locked + # Install git-cliff for changelog generation + 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 "github-actions[bot]" + git config user.email "github-actions[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 + uses: mislav/bump-homebrew-formula-action@v3 + with: + formula-name: sx + homebrew-tap: agentic-dev3o/homebrew-sx + tag-name: v${{ needs.validate.outputs.version }} + commit-message: | + {{formulaName}} {{version}} + + Created by https://github.com/mislav/bump-homebrew-formula-action + env: + COMMITTER_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.gitignore b/.gitignore index dfa83ca..c001316 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +actionlint diff --git a/README.md b/README.md index 917038e..a13e72b 100644 --- a/README.md +++ b/README.md @@ -130,13 +130,16 @@ sx --dry-run rust # Preview sandbox profile | Option | Description | |--------|-------------| | `-v, --verbose` | Show sandbox configuration | -| `-t, --trace` | Trace sandbox violations in real-time | +| `-t, --trace` | Trace sandbox violations in real-time (see note below) | +| `--trace-file ` | Write trace output to file instead of stderr | | `-n, --dry-run` | Print sandbox profile without executing | | `--explain` | Show what would be allowed/denied | | `--init` | Create `.sandbox.toml` in current directory | | `--allow-read ` | Allow read access to path | | `--allow-write ` | Allow write access to path | +> **Note on `--trace`:** The trace output shows sandbox violations from **all sandboxed processes** on the system, not just the current session. This is a limitation of macOS sandbox logging, which doesn't include session identifiers in denial logs. If you're running multiple `sx` sessions simultaneously, violations from all sessions will appear in each trace output. + ## Profiles Profiles are composable configurations that stack together: diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..f39c2a8 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,47 @@ +# git-cliff configuration for changelog generation +# https://git-cliff.org/docs/configuration + +[changelog] +header = """ +# Changelog + +All notable changes to this project will be documented in this file. + +""" +body = """ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}**{{ commit.scope }}:** {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\ + {% endfor %} +{% endfor %}\n +""" +footer = """ +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_preprocessors = [] +commit_parsers = [ + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^refactor", group = "Refactor" }, + { message = "^style", group = "Styling" }, + { message = "^test", group = "Testing" }, + { message = "^build", group = "Build" }, + { message = "^ci", group = "CI" }, + { message = "^chore\\(release\\)", skip = true }, + { message = "^chore\\(deps\\)", skip = true }, + { message = "^chore", group = "Miscellaneous" }, +] +protect_breaking_commits = false +filter_commits = false +topo_order = false +sort_commits = "oldest" diff --git a/src/cli/args.rs b/src/cli/args.rs index 34839a9..b6ee904 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -40,11 +40,14 @@ pub struct Args { #[arg(short, long)] pub debug: bool, - /// Trace sandbox violations (shows blocked operations in real-time) + /// Trace sandbox violations (shows blocked operations in real-time). + /// Note: Shows violations from ALL sandboxed processes on the system, + /// not just this session (macOS limitation) #[arg(short, long)] pub trace: bool, - /// Write trace output to file instead of stderr + /// Write trace output to file instead of stderr. + /// Note: Shows violations from ALL sandboxed processes on the system #[arg(long, value_name = "PATH")] pub trace_file: Option,