Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .github/workflows/develop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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 .goreleaser/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 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 macOS artifacts
if: ${{ steps.secrets_check.outputs.has_signing_cert == 'true' }}
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
KEYCHAIN_PATH: ${{ runner.temp }}/build.keychain-db
run: |
set -e
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
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"
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
120 changes: 115 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ jobs:
go-version-file: go.mod
cache: true

- 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)."
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:
Expand Down Expand Up @@ -76,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"
Expand All @@ -89,7 +108,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 "$CONFIG" --clean --parallelism 2

- name: Create GitHub draft release
if: ${{ startsWith(github.ref, 'refs/tags/') && inputs.snapshot != true }}
Expand Down Expand Up @@ -127,11 +146,60 @@ 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
needs: release
if: ${{ inputs.snapshot != true }}
timeout-minutes: 30

steps:
Expand Down Expand Up @@ -176,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:
Expand Down Expand Up @@ -237,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'))
Expand All @@ -249,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
Expand All @@ -262,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
Expand Down
Loading