From 91e21088e9abcf03e7a093573299c88923b2d455 Mon Sep 17 00:00:00 2001 From: ndenny Date: Thu, 11 Jun 2026 18:08:38 -0700 Subject: [PATCH 01/12] Add configuration files for production and QC environments; update main.go to reference them --- .github/workflows/develop.yml | 189 ++++++++++++++++++++++++++++++++++ config_prod.go | 9 ++ config_qc.go | 9 ++ main.go | 14 +-- 4 files changed, 210 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/develop.yml create mode 100644 config_prod.go create mode 100644 config_qc.go diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml new file mode 100644 index 0000000..2184489 --- /dev/null +++ b/.github/workflows/develop.yml @@ -0,0 +1,189 @@ +name: Develop Build + +on: + push: + branches: + - develop + +permissions: + contents: read + id-token: write # required for Workload Identity Federation (signing only) + +jobs: + build: + name: Build QC artifacts + runs-on: ubuntu-latest + timeout-minutes: 60 + + steps: + - name: Check out repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + + - name: Run GoReleaser (all platforms, unsigned) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + docker run --rm \ + -v "${PWD}:/workspace" \ + -w /workspace \ + -e GITHUB_TOKEN \ + ghcr.io/goreleaser/goreleaser-cross:v1.25-v2.12.7 \ + release --snapshot --config dist/config-qc.yaml --clean --parallelism 2 + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: qc-artifacts-${{ github.run_number }} + if-no-files-found: error + path: | + dist/*.tar.gz + dist/*.zip + dist/checksums.txt + dist/artifacts.json + dist/metadata.json + + sign-macos: + name: Sign and notarize macOS artifacts + runs-on: macos-latest + needs: build + timeout-minutes: 30 + + steps: + - name: Check out repository + uses: actions/checkout@v5 + + - name: Check for Apple signing secrets + id: secrets_check + env: + APPLE_CERT_P12: ${{ secrets.APPLE_CERT_P12 }} + run: | + if [ -n "$APPLE_CERT_P12" ]; then + echo "has_signing_cert=true" >> "$GITHUB_OUTPUT" + else + echo "has_signing_cert=false" >> "$GITHUB_OUTPUT" + echo "::warning::APPLE_CERT_P12 not configured — skipping Apple code signing." + fi + + - name: Import Apple certificate + if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }} + env: + APPLE_CERT_P12: ${{ secrets.APPLE_CERT_P12 }} + APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + set -e + for var_name in APPLE_CERT_PASSWORD KEYCHAIN_PASSWORD; do + if [ -z "${!var_name}" ]; then + echo "::error::$var_name is required when APPLE_CERT_P12 is configured." + exit 1 + fi + done + + printf '%s' "$APPLE_CERT_P12" | base64 -D > cert.p12 + + KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security import cert.p12 -k "$KEYCHAIN_PATH" -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"' | xargs) + + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: qc-artifacts-${{ github.run_number }} + path: dist + + - name: Sign and notarize macOS artifacts + if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }} + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + KEYCHAIN_PATH: ${{ runner.temp }}/build.keychain-db + run: | + set -e + for var_name in APPLE_SIGNING_IDENTITY APPLE_ID APPLE_ID_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!var_name}" ]; then + echo "::error::$var_name is required for signing and notarization." + exit 1 + fi + done + if [ ! -f dist/checksums.txt ]; then + echo "::error::dist/checksums.txt not found." + exit 1 + fi + + for archive in dist/*-darwin-*.tar.gz; do + [ -f "$archive" ] || continue + tmpdir="$(mktemp -d)" + orig_members=$(tar -tzf "$archive") + tar -xzf "$archive" -C "$tmpdir" + + while IFS= read -r -d '' bin; do + codesign --force --options runtime --timestamp \ + --keychain "$KEYCHAIN_PATH" \ + --sign "$APPLE_SIGNING_IDENTITY" "$bin" + codesign --verify --deep --strict "$bin" + + zip_dir="$(mktemp -d)" + zip_path="$zip_dir/binary.zip" + zip -j "$zip_path" "$bin" + xcrun notarytool submit "$zip_path" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_ID_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + rm -rf "$zip_dir" + done < <(find "$tmpdir" -type f -perm -111 -print0) + + # shellcheck disable=SC2086 + tar -czf "${archive}.signed" -C "$tmpdir" $orig_members + mv "${archive}.signed" "$archive" + rm -rf "$tmpdir" + + filename="$(basename "$archive")" + sha="$(shasum -a 256 "$archive" | awk '{print $1}')" + grep -v " $filename\$" dist/checksums.txt > dist/checksums.new || true + printf "%s %s\n" "$sha" "$filename" >> dist/checksums.new + mv dist/checksums.new dist/checksums.txt + + PATCH_NAME="$filename" PATCH_SHA="$sha" python3 << 'PYEOF' + import json, os + data = json.load(open('dist/artifacts.json')) + name = os.environ['PATCH_NAME'] + checksum = os.environ['PATCH_SHA'] + for a in data: + if a.get('name') == name: + a.setdefault('extra', {})['Checksum'] = 'sha256:' + checksum + with open('dist/artifacts.json', 'w') as f: + json.dump(data, f, indent=2) + PYEOF + done + + - name: Upload signed macOS artifacts + if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: qc-artifacts-macos-signed-${{ github.run_number }} + if-no-files-found: error + path: | + dist/*-darwin-amd64.tar.gz + dist/*-darwin-arm64.tar.gz + dist/checksums.txt + + - name: Clean up keychain and cert + if: always() + run: | + security delete-keychain "$RUNNER_TEMP/build.keychain-db" 2>/dev/null || true + rm -f cert.p12 diff --git a/config_prod.go b/config_prod.go new file mode 100644 index 0000000..ef370a8 --- /dev/null +++ b/config_prod.go @@ -0,0 +1,9 @@ +//go:build prod + +package main + +var baseURL = "https://client.apimetrics.io" +var authURL = "https://auth.apimetrics.io/authorize" +var tokenURL = "https://auth.apimetrics.io/oauth/token" +var authAudience = "https://client.apimetrics.io" +var clientID = "dPbV4VPvioF4nZ3oGQMn7n1vE2pFNAAI" diff --git a/config_qc.go b/config_qc.go new file mode 100644 index 0000000..fea0917 --- /dev/null +++ b/config_qc.go @@ -0,0 +1,9 @@ +//go:build !prod + +package main + +var baseURL = "https://qc-client.apimetrics.io" +var authURL = "https://qc-auth.apimetrics.io/authorize" +var tokenURL = "https://qc-auth.apimetrics.io/oauth/token" +var authAudience = "https://client.apimetrics.io" +var clientID = "bj0yh0AjBMzfeOpffmCj5UP8FbmYDwcM" diff --git a/main.go b/main.go index db8bc01..caed2aa 100644 --- a/main.go +++ b/main.go @@ -11,17 +11,9 @@ import ( ) var version string = "dev" -var commit string -var date string - -// Build-time API configuration — override with -ldflags at build time: -// -// go build -ldflags "-X main.baseURL=https://client.apimetrics.io ..." -var baseURL = "https://qc-client.apimetrics.io" -var authURL = "https://qc-auth.apimetrics.io/authorize" -var tokenURL = "https://qc-auth.apimetrics.io/oauth/token" -var authAudience = "https://client.apimetrics.io" -var clientID = "bj0yh0AjBMzfeOpffmCj5UP8FbmYDwcM" + +// baseURL, authURL, tokenURL, authAudience, clientID are defined in +// config_qc.go (default) or config_prod.go (build tag: prod). func main() { if version == "dev" { From afe05a460c6dc9e3ffaafb1285dfb35fd70d89a2 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 13:31:34 -0700 Subject: [PATCH 02/12] Formatting --- bench_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bench_test.go b/bench_test.go index a4ed1a0..2bf729a 100644 --- a/bench_test.go +++ b/bench_test.go @@ -6,10 +6,10 @@ import ( "net/http" "testing" - "github.com/amzn/ion-go/ion" - "github.com/fxamacker/cbor/v2" "apicontext.com/apimetrics/cli" "apicontext.com/apimetrics/openapi" + "github.com/amzn/ion-go/ion" + "github.com/fxamacker/cbor/v2" "github.com/shamaton/msgpack/v2" "github.com/spf13/cobra" ) From 73094efb1f58633136063f88f7030e66ff706bed Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 14:21:04 -0700 Subject: [PATCH 03/12] Switch to QC CLI Auth0 client --- config_qc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_qc.go b/config_qc.go index fea0917..9347638 100644 --- a/config_qc.go +++ b/config_qc.go @@ -6,4 +6,4 @@ var baseURL = "https://qc-client.apimetrics.io" var authURL = "https://qc-auth.apimetrics.io/authorize" var tokenURL = "https://qc-auth.apimetrics.io/oauth/token" var authAudience = "https://client.apimetrics.io" -var clientID = "bj0yh0AjBMzfeOpffmCj5UP8FbmYDwcM" +var clientID = "4fhqu4lEH5ExaRh00X1B9WJSkjTnUmuK" From ad6be2135100a0bd48f7537855a93ba126a98a26 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 14:32:48 -0700 Subject: [PATCH 04/12] Add dev build for testing locally --- config_dev.go | 9 +++++++++ config_qc.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 config_dev.go diff --git a/config_dev.go b/config_dev.go new file mode 100644 index 0000000..a6f856d --- /dev/null +++ b/config_dev.go @@ -0,0 +1,9 @@ +//go:build dev && !prod + +package main + +var baseURL = "http://localhost:8080" +var authURL = "https://local-apimetrics.auth0.com/authorize" +var tokenURL = "https://local-apimetrics.auth0.com/oauth/token" +var authAudience = "https://apimetrics-qc.appspot.com" +var clientID = "bpplXMDCn187JipRLl6Y9KrsTVZCJTbS" diff --git a/config_qc.go b/config_qc.go index 9347638..2691890 100644 --- a/config_qc.go +++ b/config_qc.go @@ -1,4 +1,4 @@ -//go:build !prod +//go:build !prod && !dev package main From d1df2491417375428d86168f4669603ed500da83 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 14:45:27 -0700 Subject: [PATCH 05/12] Add styling to auth pages --- oauth/authcode.go | 200 ++++++++++++++++++++++++++++------------------ 1 file changed, 123 insertions(+), 77 deletions(-) diff --git a/oauth/authcode.go b/oauth/authcode.go index bde8881..94ad855 100644 --- a/oauth/authcode.go +++ b/oauth/authcode.go @@ -22,105 +22,151 @@ import ( ) var htmlSuccess = ` - + + + + + + Login Successful — APImetrics + + + + -
-
-

Login Successful!

- Please return to the terminal. You may now close this window. -

+
+ +
+

Login Successful!

+

Please return to the terminal. You may now close this window.

` var htmlError = ` - + + + + + + Login Failed — APImetrics + + + + -
-
-

Error: $ERROR

- $DETAILS -

+
+ +
+

Login Failed

+

$ERROR
$DETAILS

From 61f141dd7c2f8ac2bea21caf824538f223b28e48 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 14:56:05 -0700 Subject: [PATCH 06/12] Restrict prod releases to main or main-* branches --- .github/workflows/release.yml | 15 ++++++- .goreleaser.yml | 80 ----------------------------------- 2 files changed, 14 insertions(+), 81 deletions(-) delete mode 100644 .goreleaser.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d4a0d2..4d72347 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,6 +37,19 @@ jobs: go-version-file: go.mod cache: true + - name: Verify release branch + run: | + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + # Allow main or a main- branch (e.g. main-hotfix) for emergency releases. + if ! git branch -r --contains "$GITHUB_SHA" | grep -qE '^\s*origin/main($|-)'; then + echo "::error::Tag $GITHUB_REF_NAME must be on the main branch (or a main- branch)." + exit 1 + fi + elif [[ "$GITHUB_REF" != "refs/heads/main" && "$GITHUB_REF" != refs/heads/main-* && "${{ inputs.snapshot }}" != "true" ]]; then + echo "::error::Release workflow must be dispatched from the main branch (or a main- branch, or use snapshot=true for ad-hoc builds)." + exit 1 + fi + - name: Validate dispatch inputs if: github.event_name == 'workflow_dispatch' env: @@ -89,7 +102,7 @@ jobs: -w /workspace \ -e GITHUB_TOKEN \ ghcr.io/goreleaser/goreleaser-cross:v1.25-v2.12.7 \ - release $EXTRA --clean --parallelism 2 + release $EXTRA --config dist/config.yaml --clean --parallelism 2 - name: Create GitHub draft release if: ${{ startsWith(github.ref, 'refs/tags/') && inputs.snapshot != true }} diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 21b5759..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,80 +0,0 @@ -version: 2 - -project_name: apimetrics - -before: - hooks: - - go mod download - # TODO: Figure out how to test with CGO on all platforms. - - go test -v ./... - -builds: - - id: apimetrics-darwin-amd64 - goos: - - darwin - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=o64-clang - - CXX=o64-clang++ - - - id: apimetrics-darwin-arm64 - goos: - - darwin - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=oa64-clang - - CXX=oa64-clang++ - - - id: apimetrics-linux-amd64 - goos: - - linux - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=x86_64-linux-gnu-gcc - - CXX=x86_64-linux-gnu-g++ - - - id: apimetrics-linux-arm64 - goos: - - linux - goarch: - - arm64 - env: - - CGO_ENABLED=1 - - CC=aarch64-linux-gnu-gcc - - CXX=aarch64-linux-gnu-g++ - - - id: apimetrics-windows-amd64 - goos: - - windows - goarch: - - amd64 - env: - - CGO_ENABLED=1 - - CC=x86_64-w64-mingw32-gcc - - CXX=x86_64-w64-mingw32-g++ - -archives: - - name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}" - format_overrides: - - goos: windows - formats: - - zip - -checksum: - name_template: "checksums.txt" - -snapshot: - version_template: "{{ .Tag }}-next" - -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" From f06c1aa40a787daafdcd8c3b0531f2db5034c002 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 15:17:29 -0700 Subject: [PATCH 07/12] Move goreleaser configs out of gitignored dist/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This branch replaced the auto-discovered root .goreleaser.yml with two env-specific configs but placed them under dist/, which is gitignored, so they never reached CI — the release job failed with "open dist/config.yaml: no such file or directory". (goreleaser's --clean also wipes dist/ at startup, so committing them there would not work either.) Relocate both to a tracked .goreleaser/ directory and point the release and develop workflows at the new paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/develop.yml | 2 +- .github/workflows/release.yml | 2 +- .goreleaser/config-qc.yaml | 128 +++++++++++++++ .goreleaser/config.yaml | 287 ++++++++++++++++++++++++++++++++++ 4 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 .goreleaser/config-qc.yaml create mode 100644 .goreleaser/config.yaml diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 2184489..525e128 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -36,7 +36,7 @@ jobs: -w /workspace \ -e GITHUB_TOKEN \ ghcr.io/goreleaser/goreleaser-cross:v1.25-v2.12.7 \ - release --snapshot --config dist/config-qc.yaml --clean --parallelism 2 + release --snapshot --config .goreleaser/config-qc.yaml --clean --parallelism 2 - name: Upload build artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d72347..31bc59d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,7 +102,7 @@ jobs: -w /workspace \ -e GITHUB_TOKEN \ ghcr.io/goreleaser/goreleaser-cross:v1.25-v2.12.7 \ - release $EXTRA --config dist/config.yaml --clean --parallelism 2 + release $EXTRA --config .goreleaser/config.yaml --clean --parallelism 2 - name: Create GitHub draft release if: ${{ startsWith(github.ref, 'refs/tags/') && inputs.snapshot != true }} diff --git a/.goreleaser/config-qc.yaml b/.goreleaser/config-qc.yaml new file mode 100644 index 0000000..85a699f --- /dev/null +++ b/.goreleaser/config-qc.yaml @@ -0,0 +1,128 @@ +version: 2 +project_name: apimetrics-qc +builds: + - id: apimetrics-qc-darwin-amd64 + goos: + - darwin + goarch: + - amd64 + targets: + - darwin_amd64_v1 + dir: . + main: . + binary: apimetrics-qc + builder: go + tool: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=o64-clang + - CXX=o64-clang++ + - id: apimetrics-qc-darwin-arm64 + goos: + - darwin + goarch: + - arm64 + targets: + - darwin_arm64_v8.0 + dir: . + main: . + binary: apimetrics-qc + builder: go + tool: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=oa64-clang + - CXX=oa64-clang++ + - id: apimetrics-qc-linux-amd64 + goos: + - linux + goarch: + - amd64 + targets: + - linux_amd64_v1 + dir: . + main: . + binary: apimetrics-qc + builder: go + tool: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + - CXX=x86_64-linux-gnu-g++ + - id: apimetrics-qc-linux-arm64 + goos: + - linux + goarch: + - arm64 + targets: + - linux_arm64_v8.0 + dir: . + main: . + binary: apimetrics-qc + builder: go + tool: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + - id: apimetrics-qc-windows-amd64 + goos: + - windows + goarch: + - amd64 + targets: + - windows_amd64_v1 + dir: . + main: . + binary: apimetrics-qc + builder: go + tool: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + - CXX=x86_64-w64-mingw32-g++ +archives: + - id: default + builds_info: + mode: 493 + name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}' + formats: + - tar.gz + format_overrides: + - goos: windows + formats: + - zip + files: + - src: license* + - src: LICENSE* + - src: readme* + - src: README* +snapshot: + version_template: '{{ .ShortCommit }}' +checksum: + name_template: checksums.txt + algorithm: sha256 +dist: dist +gomod: + gobinary: go +before: + hooks: + - go mod download + - go test -v ./... +git: + tag_sort: -version:refname diff --git a/.goreleaser/config.yaml b/.goreleaser/config.yaml new file mode 100644 index 0000000..55927d7 --- /dev/null +++ b/.goreleaser/config.yaml @@ -0,0 +1,287 @@ +version: 2 +project_name: apimetrics +release: + github: + owner: APImetrics + name: APImetrics-cli + name_template: '{{.Tag}}' +builds: + - id: apimetrics-darwin-amd64 + goos: + - darwin + goarch: + - amd64 + goamd64: + - v1 + go386: + - sse2 + goarm: + - "6" + goarm64: + - v8.0 + gomips: + - hardfloat + goppc64: + - power8 + goriscv64: + - rva20u64 + targets: + - darwin_amd64_v1 + dir: . + main: . + binary: apimetrics + builder: go + tool: go + command: build + flags: + - -tags=prod + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=o64-clang + - CXX=o64-clang++ + - id: apimetrics-darwin-arm64 + goos: + - darwin + goarch: + - arm64 + goamd64: + - v1 + go386: + - sse2 + goarm: + - "6" + goarm64: + - v8.0 + gomips: + - hardfloat + goppc64: + - power8 + goriscv64: + - rva20u64 + targets: + - darwin_arm64_v8.0 + dir: . + main: . + binary: apimetrics + builder: go + tool: go + command: build + flags: + - -tags=prod + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=oa64-clang + - CXX=oa64-clang++ + - id: apimetrics-linux-amd64 + goos: + - linux + goarch: + - amd64 + goamd64: + - v1 + go386: + - sse2 + goarm: + - "6" + goarm64: + - v8.0 + gomips: + - hardfloat + goppc64: + - power8 + goriscv64: + - rva20u64 + targets: + - linux_amd64_v1 + dir: . + main: . + binary: apimetrics + builder: go + tool: go + command: build + flags: + - -tags=prod + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + - CXX=x86_64-linux-gnu-g++ + - id: apimetrics-linux-arm64 + goos: + - linux + goarch: + - arm64 + goamd64: + - v1 + go386: + - sse2 + goarm: + - "6" + goarm64: + - v8.0 + gomips: + - hardfloat + goppc64: + - power8 + goriscv64: + - rva20u64 + targets: + - linux_arm64_v8.0 + dir: . + main: . + binary: apimetrics + builder: go + tool: go + command: build + flags: + - -tags=prod + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + - CXX=aarch64-linux-gnu-g++ + - id: apimetrics-windows-amd64 + goos: + - windows + goarch: + - amd64 + goamd64: + - v1 + go386: + - sse2 + goarm: + - "6" + goarm64: + - v8.0 + gomips: + - hardfloat + goppc64: + - power8 + goriscv64: + - rva20u64 + targets: + - windows_amd64_v1 + dir: . + main: . + binary: apimetrics + builder: go + tool: go + command: build + flags: + - -tags=prod + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + - CXX=x86_64-w64-mingw32-g++ +archives: + - id: default + builds_info: + mode: 493 + name_template: '{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}' + formats: + - tar.gz + format_overrides: + - goos: windows + formats: + - zip + files: + - src: license* + - src: LICENSE* + - src: readme* + - src: README* + - src: changelog* + - src: CHANGELOG* +snapshot: + version_template: '{{ .Tag }}-next' +checksum: + name_template: checksums.txt + algorithm: sha256 +docker_digest: + name_template: digests.txt +blobs: + - bucket: apimetrics-cli + provider: gs + directory: '{{ .Version }}' + content_disposition: attachment;filename={{.Filename}} +changelog: + filters: + exclude: + - '^docs:' + - '^test:' + sort: asc + format: '{{ .SHA }} {{ .Message }}' +dist: dist +env_files: + github_token: ~/.config/goreleaser/github_token + gitlab_token: ~/.config/goreleaser/gitlab_token + gitea_token: ~/.config/goreleaser/gitea_token +before: + hooks: + - go mod download + - go test -v ./... +source: + name_template: '{{ .ProjectName }}-{{ .Version }}' + format: tar.gz +gomod: + gobinary: go +announce: + twitter: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + mastodon: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + server: "" + reddit: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + url_template: '{{ .ReleaseURL }}' + slack: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + username: GoReleaser + discord: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + author: GoReleaser + color: "3888754" + icon_url: https://goreleaser.com/static/avatar.png + teams: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + color: '#2D313E' + icon_url: https://goreleaser.com/static/avatar.png + smtp: + subject_template: '{{ .ProjectName }} {{ .Tag }} is out!' + body_template: 'You can view details from: {{ .ReleaseURL }}' + mattermost: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + username: GoReleaser + linkedin: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + telegram: + message_template: '{{ mdv2escape .ProjectName }} {{ mdv2escape .Tag }} is out{{ mdv2escape "!" }} Check it out at {{ mdv2escape .ReleaseURL }}' + parse_mode: MarkdownV2 + webhook: + message_template: '{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}' + content_type: application/json; charset=utf-8 + expected_status_codes: + - 200 + - 201 + - 202 + - 204 + opencollective: + title_template: '{{ .Tag }}' + message_template: '{{ .ProjectName }} {{ .Tag }} is out!
Check it out at {{ .ReleaseURL }}' + bluesky: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' +git: + tag_sort: -version:refname +github_urls: + download: https://github.com +gitlab_urls: + download: https://gitlab.com From 3a000bc05f6b4f4c03d5c8a9599a2bf985ff9861 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 15:44:06 -0700 Subject: [PATCH 08/12] Escape OAuth error params and harden release branch check - ServeHTTP now HTML-escapes the error/error_description query params before interpolating them into the error page, preventing reflected XSS via the localhost redirect URL. - The release tag guard fetches origin/main* explicitly before git branch --contains, since tag pushes don't populate remote-tracking refs and the check would otherwise fail on legitimate main tags. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 3 +++ oauth/authcode.go | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31bc59d..c387c61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,9 @@ jobs: - name: Verify release branch run: | if [[ "$GITHUB_REF" == refs/tags/* ]]; then + # Tag pushes don't populate origin/* remote-tracking refs, so fetch + # the main branches explicitly before checking containment. + git fetch --quiet origin '+refs/heads/main:refs/remotes/origin/main' '+refs/heads/main-*:refs/remotes/origin/main-*' || true # Allow main or a main- branch (e.g. main-hotfix) for emergency releases. if ! git branch -r --contains "$GITHUB_SHA" | grep -qE '^\s*origin/main($|-)'; then echo "::error::Tag $GITHUB_REF_NAME must be on the main branch (or a main- branch)." diff --git a/oauth/authcode.go b/oauth/authcode.go index 94ad855..5ff2eef 100644 --- a/oauth/authcode.go +++ b/oauth/authcode.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "html" "net/http" "net/url" "os" @@ -233,7 +234,9 @@ func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := r.URL.Query().Get("error"); err != "" { details := r.URL.Query().Get("error_description") - rendered := strings.Replace(strings.Replace(htmlError, "$ERROR", err, 1), "$DETAILS", details, 1) + // Escape the query params before interpolating into HTML to avoid + // reflected XSS via the localhost redirect URL. + rendered := strings.Replace(strings.Replace(htmlError, "$ERROR", html.EscapeString(err), 1), "$DETAILS", html.EscapeString(details), 1) w.Write([]byte(rendered)) h.c <- "" return From cfbd5d95d575b64bcdc4a4742de1c1f50f55710b Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 15:49:20 -0700 Subject: [PATCH 09/12] Build QC artifacts on snapshot dispatch and attach them Snapshot (--snapshot) dispatches now build with config-qc.yaml so they produce QC binaries, and the resulting dist artifacts are uploaded to the workflow run as qc-snapshot-artifacts for download. Previously snapshot runs used the prod config and uploaded nothing. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c387c61..8989e8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,9 +92,12 @@ jobs: run: | set -e EXTRA="" + CONFIG=".goreleaser/config.yaml" if [ "$SNAPSHOT" = "true" ]; then + # Snapshot builds are QC builds: QC endpoints, apimetrics-qc binary. EXTRA="--snapshot --skip homebrew" - echo "Running goreleaser in snapshot mode (no publish)" + CONFIG=".goreleaser/config-qc.yaml" + echo "Running goreleaser in snapshot mode (QC build, no publish)" elif [[ "$GITHUB_REF" == refs/tags/* ]]; then EXTRA="--skip publish,homebrew" echo "Building artifacts without publishing; GitHub release and GCS upload deferred to finalization" @@ -105,7 +108,7 @@ jobs: -w /workspace \ -e GITHUB_TOKEN \ ghcr.io/goreleaser/goreleaser-cross:v1.25-v2.12.7 \ - release $EXTRA --config .goreleaser/config.yaml --clean --parallelism 2 + release $EXTRA --config "$CONFIG" --clean --parallelism 2 - name: Create GitHub draft release if: ${{ startsWith(github.ref, 'refs/tags/') && inputs.snapshot != true }} @@ -131,10 +134,9 @@ jobs: fi - name: Upload release artifacts - if: ${{ inputs.snapshot != true }} uses: actions/upload-artifact@v4 with: - name: release-artifacts + name: ${{ inputs.snapshot == true && 'qc-snapshot-artifacts' || 'release-artifacts' }} if-no-files-found: error path: | dist/*.tar.gz From d0e78f6ebeff96863252991c4ae6dad261f0ccbe Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 16:17:46 -0700 Subject: [PATCH 10/12] Upload QC snapshot assets individually Snapshot runs previously bundled every artifact under a single qc-snapshot-artifacts entry, which GitHub serves as one combined zip. Upload each platform archive (and checksums.txt) as its own artifact so the Actions run lists them individually. Non-snapshot release runs keep the combined release-artifacts bundle the macOS job depends on. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 53 ++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8989e8c..cd63a60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,9 +134,10 @@ jobs: fi - name: Upload release artifacts + if: ${{ inputs.snapshot != true }} uses: actions/upload-artifact@v4 with: - name: ${{ inputs.snapshot == true && 'qc-snapshot-artifacts' || 'release-artifacts' }} + name: release-artifacts if-no-files-found: error path: | dist/*.tar.gz @@ -145,6 +146,56 @@ jobs: dist/artifacts.json dist/metadata.json + # Snapshot (QC) builds: upload each asset as its own artifact so the + # Actions run lists them individually rather than as one combined zip. + - name: Upload QC snapshot — macOS (Intel) + if: ${{ inputs.snapshot == true }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-darwin-amd64 + if-no-files-found: error + path: dist/*-darwin-amd64.tar.gz + + - name: Upload QC snapshot — macOS (Apple Silicon) + if: ${{ inputs.snapshot == true }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-darwin-arm64 + if-no-files-found: error + path: dist/*-darwin-arm64.tar.gz + + - name: Upload QC snapshot — Linux (amd64) + if: ${{ inputs.snapshot == true }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-linux-amd64 + if-no-files-found: error + path: dist/*-linux-amd64.tar.gz + + - name: Upload QC snapshot — Linux (arm64) + if: ${{ inputs.snapshot == true }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-linux-arm64 + if-no-files-found: error + path: dist/*-linux-arm64.tar.gz + + - name: Upload QC snapshot — Windows (amd64) + if: ${{ inputs.snapshot == true }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-windows-amd64 + if-no-files-found: error + path: dist/*-windows-amd64.zip + + - name: Upload QC snapshot — checksums + if: ${{ inputs.snapshot == true }} + uses: actions/upload-artifact@v4 + with: + name: checksums.txt + if-no-files-found: error + path: dist/checksums.txt + release-macos: name: Sign and notarize macOS artifacts runs-on: macos-latest From 5d2cb788cdbd1e1d47d7ce76fe3d74afe112ae59 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 16:23:23 -0700 Subject: [PATCH 11/12] Sign macOS builds in snapshot mode and upload them as assets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the release-macos job was gated off entirely for snapshot (QC) builds, so snapshot darwin archives were never signed and only the unsigned placeholders from the build job were available. Now the macOS job runs for snapshot dispatches too: it pulls the individually-uploaded QC artifacts, signs/notarizes the darwin archives, and re-uploads the signed archives (and updated checksums.txt) as individual assets, overwriting the unsigned placeholders. The tag-release path is unchanged — it still downloads the combined release-artifacts bundle and produces macos-signed-artifacts. The artifacts.json checksum patch is skipped when that file is absent (snapshot builds). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 47 ++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd63a60..208409f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -200,7 +200,6 @@ jobs: name: Sign and notarize macOS artifacts runs-on: macos-latest needs: release - if: ${{ inputs.snapshot != true }} timeout-minutes: 30 steps: @@ -245,11 +244,21 @@ jobs: security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"' | xargs) - name: Download release artifacts + if: ${{ inputs.snapshot != true }} uses: actions/download-artifact@v4 with: name: release-artifacts path: dist + # Snapshot builds upload each asset as its own artifact; pull them all + # back (flattened) so the signing step finds the darwin archives. + - name: Download QC snapshot artifacts + if: ${{ inputs.snapshot == true }} + uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: dist + - name: Sign and notarize macOS artifacts if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }} env: @@ -306,7 +315,9 @@ jobs: printf "%s %s\n" "$sha" "$filename" >> dist/checksums.new mv dist/checksums.new dist/checksums.txt - # Keep artifacts.json in sync with the signed checksums for archival accuracy. + # Keep artifacts.json in sync with the signed checksums for archival + # accuracy. Only present for full releases, not snapshot builds. + if [ -f dist/artifacts.json ]; then PATCH_NAME="$filename" PATCH_SHA="$sha" python3 << 'PYEOF' import json, os data = json.load(open('dist/artifacts.json')) @@ -318,10 +329,11 @@ jobs: with open('dist/artifacts.json', 'w') as f: json.dump(data, f, indent=2) PYEOF + fi done - name: Upload signed macOS artifacts - if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }} + if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' && inputs.snapshot != true }} uses: actions/upload-artifact@v4 with: name: macos-signed-artifacts @@ -331,6 +343,35 @@ jobs: dist/*-darwin-arm64.tar.gz dist/checksums.txt + # Snapshot builds: replace the unsigned darwin placeholders uploaded by + # the build job with the signed archives, listed as individual assets. + - name: Upload signed QC snapshot — macOS (Intel) + if: ${{ inputs.snapshot == true && steps.secrets_check.outputs.has_signing_cert == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-darwin-amd64 + overwrite: true + if-no-files-found: error + path: dist/*-darwin-amd64.tar.gz + + - name: Upload signed QC snapshot — macOS (Apple Silicon) + if: ${{ inputs.snapshot == true && steps.secrets_check.outputs.has_signing_cert == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: apimetrics-qc-darwin-arm64 + overwrite: true + if-no-files-found: error + path: dist/*-darwin-arm64.tar.gz + + - name: Upload signed QC snapshot — checksums + if: ${{ inputs.snapshot == true && steps.secrets_check.outputs.has_signing_cert == 'true' }} + uses: actions/upload-artifact@v4 + with: + name: checksums.txt + overwrite: true + if-no-files-found: error + path: dist/checksums.txt + - name: Authenticate to Google Cloud if: ${{ startsWith(github.ref, 'refs/tags/') }} uses: google-github-actions/auth@v2 From 6a158aca2a974e7006aba1536e2a8cf113cbe879 Mon Sep 17 00:00:00 2001 From: ndenny Date: Fri, 12 Jun 2026 16:30:19 -0700 Subject: [PATCH 12/12] Make develop builds sign-only (no notarization) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merges to develop run develop.yml on every push, so notarizing the macOS builds each time wastes time and Apple notary quota — those are internal QC artifacts, not externally distributed. Drop the notarytool submit step and the now-unused APPLE_ID/APPLE_ID_PASSWORD/APPLE_TEAM_ID env vars, keeping codesign + verify. Notarization is retained for snapshot dispatches and tagged releases in release.yml. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/develop.yml | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 525e128..a9e2a54 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -51,7 +51,7 @@ jobs: dist/metadata.json sign-macos: - name: Sign and notarize macOS artifacts + name: Sign macOS artifacts runs-on: macos-latest needs: build timeout-minutes: 30 @@ -103,22 +103,17 @@ jobs: name: qc-artifacts-${{ github.run_number }} path: dist - - name: Sign and notarize macOS artifacts + - name: Sign macOS artifacts if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }} env: APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} KEYCHAIN_PATH: ${{ runner.temp }}/build.keychain-db run: | set -e - for var_name in APPLE_SIGNING_IDENTITY APPLE_ID APPLE_ID_PASSWORD APPLE_TEAM_ID; do - if [ -z "${!var_name}" ]; then - echo "::error::$var_name is required for signing and notarization." - exit 1 - fi - done + if [ -z "$APPLE_SIGNING_IDENTITY" ]; then + echo "::error::APPLE_SIGNING_IDENTITY is required for signing." + exit 1 + fi if [ ! -f dist/checksums.txt ]; then echo "::error::dist/checksums.txt not found." exit 1 @@ -135,16 +130,6 @@ jobs: --keychain "$KEYCHAIN_PATH" \ --sign "$APPLE_SIGNING_IDENTITY" "$bin" codesign --verify --deep --strict "$bin" - - zip_dir="$(mktemp -d)" - zip_path="$zip_dir/binary.zip" - zip -j "$zip_path" "$bin" - xcrun notarytool submit "$zip_path" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait - rm -rf "$zip_dir" done < <(find "$tmpdir" -type f -perm -111 -print0) # shellcheck disable=SC2086