name: Build & Release # Trigger on version tags (e.g. git tag v1.0.0 && git push --tags) on: push: tags: - "v*" # Manual run from Actions tab (useful for testing the workflow itself) workflow_dispatch: jobs: build: strategy: fail-fast: false matrix: include: - os: macos-latest platform: macOS artifact: BetterClaudeConfig-macOS.zip - os: windows-latest platform: Windows artifact: BetterClaudeConfig-Windows.zip - os: ubuntu-latest platform: Linux artifact: BetterClaudeConfig-Linux.tar.gz runs-on: ${{ matrix.os }} name: Build (${{ matrix.platform }}) steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python 3.12 (Linux) if: runner.os == 'Linux' uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up Python venv (macOS) if: runner.os == 'macOS' run: | PYBIN="$(command -v python3.12 || echo /opt/homebrew/bin/python3.12)" "$PYBIN" -m venv .venv echo "$PWD/.venv/bin" >> "$GITHUB_PATH" - name: Set up Python venv (Windows) if: runner.os == 'Windows' shell: pwsh run: | py -3.12 -m venv .venv Add-Content -Path $env:GITHUB_PATH -Value "$env:GITHUB_WORKSPACE\.venv\Scripts" - name: Install dependencies run: pip install -r requirements-dev.txt # macOS: build app.icns from source PNGs (iconutil is built into macOS) - name: Generate app.icns (macOS) if: runner.os == 'macOS' run: python scripts/build_icons.py # Windows: generate app.ico using Pillow (already installed via requirements-dev.txt) - name: Generate app.ico (Windows) if: runner.os == 'Windows' run: python scripts/build_icons.py - name: Build with PyInstaller run: pyinstaller bcc.spec # ── Package ────────────────────────────────────────────────────────── - name: Package (macOS) if: runner.os == 'macOS' run: | cd dist zip -r --symlinks "../${{ matrix.artifact }}" BetterClaudeConfig.app - name: Package (Windows) if: runner.os == 'Windows' shell: pwsh run: | Compress-Archive -Path dist\BetterClaudeConfig.exe ` -DestinationPath "${{ matrix.artifact }}" - name: Package (Linux) if: runner.os == 'Linux' run: | tar -czf "${{ matrix.artifact }}" -C dist BetterClaudeConfig # ── Upload artifact for the release job ────────────────────────────── - name: Upload artifact uses: actions/upload-artifact@v3 with: name: ${{ matrix.artifact }} path: ${{ matrix.artifact }} # ── Signing-key smoke test (workflow_dispatch only) ───────────────────── # # The Publish job is gated on a tag, so a manual run never exercises the # signing step — which means a wrong/missing RELEASE_SIGNING_KEY secret # would only be discovered at the worst possible moment: during a real # release. This job signs a throwaway manifest with the secret and verifies # the result against the PUBLIC key already compiled into bcc_core. # # It proves the two halves of the keypair actually match, without # publishing anything. Run it from the Actions tab after setting or # rotating the secret. signing-smoke-test: name: Signing key smoke test runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install dependencies run: pip install cryptography - name: Sign a throwaway manifest and verify against the shipped pubkey env: RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} run: | if [ -z "$RELEASE_SIGNING_KEY" ]; then echo "FAIL: RELEASE_SIGNING_KEY secret is not set." echo "Generate it with: python catalog_console.py show-seed-b64" echo "then add it under Settings -> Actions -> Secrets." exit 1 fi mkdir -p smoke && echo "smoke test payload" > smoke/hello.txt python3 scripts/sign_checksums.py generate smoke --out smoke/SHA256SUMS python3 scripts/sign_checksums.py sign --sums smoke/SHA256SUMS --out smoke/SHA256SUMS.sig python - <<'PY' import base64, pathlib, sys import bcc_core as c from scripts.sign_checksums import verify_checksums # The public half that ships inside the binary. If the secret is a # DIFFERENT key than the one users' copies trust, this fails here -- # which is the entire point of the job. pub_b64 = base64.b64encode(c.CATALOG_PUBKEYS[0]).decode() sums = pathlib.Path("smoke/SHA256SUMS").read_text() sig = pathlib.Path("smoke/SHA256SUMS.sig").read_bytes() if not verify_checksums(pub_b64, sums, sig): sys.exit( "FAIL: the signature produced by RELEASE_SIGNING_KEY does NOT verify\n" "against the public key in bcc_core.CATALOG_PUBKEYS.\n" "\n" "The secret and the shipped public key are different keypairs. Users\n" "would reject every signature this CI produces. Re-copy the seed from\n" "`catalog_console.py show-seed-b64`, or update CATALOG_PUBKEYS." ) print("OK: RELEASE_SIGNING_KEY matches the public key shipped in bcc_core.") PY # ── Create GitHub Release with all three artifacts ────────────────────── release: name: Publish Release needs: build runs-on: ubuntu-latest # Only publish when a tag was pushed (not on workflow_dispatch without a tag) if: startsWith(github.ref, 'refs/tags/') permissions: contents: write steps: # Needed for scripts/sign_checksums.py — the release job otherwise # only downloads build artifacts, it doesn't check out the repo. - name: Checkout uses: actions/checkout@v4 - name: Download all artifacts uses: actions/download-artifact@v3 with: path: artifacts - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: "3.12" # download-artifact@v3 nests each artifact under a directory named # after it (artifacts//). Flatten into one directory so # SHA256SUMS lists plain filenames, matching what `sha256sum -c` # expects when run from inside an extracted release download. - name: Collect release files run: | mkdir -p release-files find artifacts -type f -exec cp {} release-files/ \; ls -la release-files - name: Generate SHA256SUMS run: python3 scripts/sign_checksums.py generate release-files --out release-files/SHA256SUMS # ── Sign the checksum manifest (best-effort) ────────────────────── # # BCC binaries are not code-signed (no budget for a paid cert). This # is the free half: a checksum manifest, detached-signed with # Ed25519, so a tampered download is detectable by anyone who # checks. It does NOT remove Gatekeeper/SmartScreen warnings. # # The private key is a repo secret (RELEASE_SIGNING_KEY, base64 raw # Ed25519 seed) generated via the Catalog Console (#62). If it's not # set, we still publish the release — just without a .sig — rather # than fail the release outright. - name: Check for signing key id: signing run: | if [ -n "${{ secrets.RELEASE_SIGNING_KEY }}" ]; then echo "has_key=true" >> "$GITHUB_OUTPUT" else echo "has_key=false" >> "$GITHUB_OUTPUT" fi - name: Install signing dependencies if: steps.signing.outputs.has_key == 'true' run: pip install cryptography - name: Sign SHA256SUMS if: steps.signing.outputs.has_key == 'true' env: RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }} run: | python3 scripts/sign_checksums.py sign \ --sums release-files/SHA256SUMS \ --out release-files/SHA256SUMS.sig - name: Warn — release will be unsigned if: steps.signing.outputs.has_key != 'true' run: | echo "::warning::RELEASE_SIGNING_KEY secret is not set — this release is being published WITHOUT a signed SHA256SUMS.sig. Add the secret (base64 raw Ed25519 seed, generated via the Catalog Console, #62) before the next tag." - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: name: Better Claude Config ${{ github.ref_name }} draft: false prerelease: false generate_release_notes: false files: | artifacts/**/* release-files/SHA256SUMS* body: | ## Better Claude Config ${{ github.ref_name }} A GUI for managing MCP server configurations for Claude Desktop and Claude Code — no hand-editing JSON. ### Download | Platform | File | |----------|------| | macOS | `BetterClaudeConfig-macOS.zip` — unzip and drag **BetterClaudeConfig.app** to Applications | | Windows | `BetterClaudeConfig-Windows.zip` — unzip and run **BetterClaudeConfig.exe** | | Linux | `BetterClaudeConfig-Linux.tar.gz` — extract and run **BetterClaudeConfig** | ### macOS note The app is not code-signed. On first launch, right-click → **Open** to bypass Gatekeeper, or run: ``` xattr -cr /Applications/BetterClaudeConfig.app ``` ### Verifying your download Every release includes `SHA256SUMS` (and, when the signing key is configured, a detached `SHA256SUMS.sig`). See [Verifying your download](https://git.avezzano.io/the_og/better-claude-config#verifying-your-download) in the README for commands. This proves you got the file we published — it does not remove Gatekeeper/SmartScreen warnings. ### Requirements No Python installation needed — the app is self-contained.