diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07bba35..14a11bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,10 @@ permissions: jobs: goreleaser: - runs-on: ubuntu-latest + # INT-446: darwin must build with cgo (Keychain backend). cgo+darwin + # cannot cross-compile from Linux, so this job runs on macOS. Pinned + # image (not the moving macos-latest label) for a reproducible release. + runs-on: macos-15 env: TAG: ${{ github.ref_name || inputs.tag }} steps: @@ -29,14 +32,85 @@ jobs: with: go-version: "1.24" - - name: Run GoReleaser + - name: Install GoReleaser uses: goreleaser/goreleaser-action@v6 with: - version: latest - args: release --clean + version: "~> v2" + install-only: true + + - name: GoReleaser check + run: goreleaser check + + - name: Build (snapshot, no publish) + run: goreleaser release --snapshot --clean + + # INT-446 pre-publish gate: prove the darwin binaries actually carry + # the Keychain backend BEFORE anything is published. A CGO_ENABLED=0 + # darwin build links no Security.framework and fails closed at + # runtime; this gate makes that impossible to ship silently. + - name: Pre-publish gate — darwin Keychain backend present + run: | + set -euo pipefail + art=dist/artifacts.json + arm_bin=$(jq -r '.[]|select(.type=="Binary" and .goos=="darwin" and .goarch=="arm64")|.path' "$art") + amd_bin=$(jq -r '.[]|select(.type=="Binary" and .goos=="darwin" and .goarch=="amd64")|.path' "$art") + [ -n "$arm_bin" ] && [ -n "$amd_bin" ] || { echo "missing a darwin binary in artifacts.json"; exit 1; } + # darwin archives: exactly one per arch, no duplicate names + tot=$(jq '[.[]|select(.type=="Archive" and .goos=="darwin")|.name]|length' "$art") + uniq=$(jq '[.[]|select(.type=="Archive" and .goos=="darwin")|.name]|unique|length' "$art") + [ "$tot" = "$uniq" ] || { echo "duplicate darwin archive names"; exit 1; } + [ "$(jq '[.[]|select(.type=="Archive" and .goos=="darwin" and .goarch=="arm64")]|length' "$art")" = 1 ] || { echo "expected exactly one darwin/arm64 archive"; exit 1; } + [ "$(jq '[.[]|select(.type=="Archive" and .goos=="darwin" and .goarch=="amd64")]|length' "$art")" = 1 ] || { echo "expected exactly one darwin/amd64 archive"; exit 1; } + # Mach-O arch sanity (both slices) + file "$arm_bin" | grep -q 'arm64' || { echo "arm64 binary is not arm64 Mach-O"; exit 1; } + file "$amd_bin" | grep -q 'x86_64' || { echo "amd64 binary is not x86_64 Mach-O"; exit 1; } + lipo -archs "$arm_bin" | grep -qw arm64 || { echo "lipo: arm64 slice missing"; exit 1; } + lipo -archs "$amd_bin" | grep -qw x86_64 || { echo "lipo: x86_64 slice missing"; exit 1; } + # amd64 cannot run on the arm64 runner: assert Security.framework + # is linked. CGO_ENABLED=0 omits it entirely, so its presence is a + # sound *necessary* cgo signal for the slice we can't execute. + otool -L "$amd_bin" | grep -q '/System/Library/Frameworks/Security.framework' \ + || { echo "amd64 binary not linked against Security.framework (cgo missing)"; exit 1; } + # arm64 authoritative functional check: with no backend override + # and a seeded config, credstore must auto-select the Keychain. + tmp=$(mktemp -d) + mkdir -p "$tmp/xdg/slack-chat-api" + printf 'credential_ref: slack-chat-api/default\nworkspace: smoke\n' > "$tmp/xdg/slack-chat-api/config.yml" + out=$(env -u SLACK_CHAT_API_KEYRING_BACKEND HOME="$tmp" XDG_CONFIG_HOME="$tmp/xdg" "$arm_bin" --output json config show) + echo "$out" + b=$(echo "$out" | jq -r '.backend'); s=$(echo "$out" | jq -r '.backend_source'); r=$(echo "$out" | jq -r '.credential_ref') + [ "$b" = "keychain" ] && [ "$s" = "auto" ] && [ "$r" = "slack-chat-api/default" ] \ + || { echo "GATE FAIL: backend=$b source=$s ref=$r (want keychain/auto/slack-chat-api/default)"; exit 1; } + echo "GATE OK: darwin/arm64 backend=keychain source=auto; darwin/amd64 Security.framework linked" + + - name: Release notes + run: | + set -euo pipefail + cat > "$RUNNER_TEMP/release-notes.md" <<'EOF' + ### macOS Keychain storage restored + + Builds since the credential-store migration were compiled without + cgo and failed closed on macOS (no Keychain backend). This release + builds the darwin binaries with cgo enabled, restoring native + macOS Keychain storage. Upgrade and re-run your normal commands; + no other action is required. + EOF + + - name: Release (publish) + run: goreleaser release --clean --release-notes="$RUNNER_TEMP/release-notes.md" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Verify release notes published + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + body=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body -q .body) + if ! printf '%s' "$body" | grep -q 'macOS Keychain'; then + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --notes-file "$RUNNER_TEMP/release-notes.md" + fi + update-homebrew: needs: goreleaser runs-on: ubuntu-latest @@ -53,6 +127,7 @@ jobs: - name: Parse checksums id: checksums run: | + set -euo pipefail VERSION="${{ env.TAG }}" VERSION_NUM="${VERSION#v}" @@ -62,6 +137,16 @@ jobs: LINUX_ARM64_SHA=$(grep "linux_arm64.tar.gz" checksums.txt | cut -d' ' -f1) LINUX_AMD64_SHA=$(grep "linux_amd64.tar.gz" checksums.txt | cut -d' ' -f1) + # Fail loudly rather than write a cask with an empty sha256 if an + # archive was renamed/missing (INT-446: blast radius of the build + # split). + for pair in \ + "darwin_arm64:$DARWIN_ARM64_SHA" "darwin_amd64:$DARWIN_AMD64_SHA" \ + "linux_arm64:$LINUX_ARM64_SHA" "linux_amd64:$LINUX_AMD64_SHA"; do + name="${pair%%:*}"; sha="${pair#*:}" + if [ -z "$sha" ]; then echo "empty checksum for ${name} archive"; exit 1; fi + done + echo "version=${VERSION_NUM}" >> $GITHUB_OUTPUT echo "darwin_arm64_sha=${DARWIN_ARM64_SHA}" >> $GITHUB_OUTPUT echo "darwin_amd64_sha=${DARWIN_AMD64_SHA}" >> $GITHUB_OUTPUT diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 555b125..2497fca 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -7,14 +7,46 @@ before: - go mod tidy - go test ./... +# INT-446: darwin builds with CGO so 99designs/keyring's Keychain backend +# (//go:build darwin && cgo) is compiled in; without it credstore fails +# closed on macOS. linux/windows stay CGO-off static (their keyring +# backends are pure Go; cgo there would regress glibc portability). +# Split is by GOOS only — both darwin amd64 and arm64 are still produced. builds: - - id: slck + - id: slck-darwin main: ./cmd/slck binary: slck env: - - CGO_ENABLED=0 + - CGO_ENABLED=1 goos: - darwin + goarch: + - amd64 + - arm64 + overrides: + - goos: darwin + goarch: amd64 + goamd64: v1 + env: + - CGO_ENABLED=1 + - "CC=xcrun clang -arch x86_64" + - goos: darwin + goarch: arm64 + goarm64: v8.0 + env: + - CGO_ENABLED=1 + - "CC=xcrun clang -arch arm64" + ldflags: + - -s -w + - -X github.com/open-cli-collective/slack-chat-api/internal/version.Version={{.Version}} + - -X github.com/open-cli-collective/slack-chat-api/internal/version.Commit={{.Commit}} + - -X github.com/open-cli-collective/slack-chat-api/internal/version.Date={{.Date}} + - id: slck-unix-win + main: ./cmd/slck + binary: slck + env: + - CGO_ENABLED=0 + goos: - linux - windows goarch: @@ -42,6 +74,11 @@ archives: # Linux packages (.deb and .rpm) nfpms: - id: slck + # deb/rpm are linux-only: pull the static CGO-off build, never darwin. + # `ids` (the v2 build-id filter) — `builds` is deprecated and fails + # `goreleaser check`. + ids: + - slck-unix-win package_name: slck vendor: Open CLI Collective homepage: https://github.com/open-cli-collective/slack-chat-api