Files
wehub-resource-sync 26f897c1ec
release / release-please (push) Failing after 1m49s
docs / build (push) Failing after 6m34s
release / build-and-upload (arm64, linux) (push) Has been cancelled
release / build-and-upload (arm64, windows) (push) Has been cancelled
release / build-darwin (amd64, darwin) (push) Has been cancelled
release / checksums (push) Has been cancelled
release / finalize (push) Has been cancelled
release / build-darwin (arm64, darwin) (push) Has been cancelled
release / build-and-upload (amd64, linux) (push) Has been cancelled
release / build-and-upload (amd64, windows) (push) Has been cancelled
docs / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:34:16 +08:00

348 lines
14 KiB
YAML

name: release
on:
push:
branches:
- main
permissions:
contents: write
pull-requests: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
# macOS artifacts are built and Developer ID signed on a macOS runner (codesign
# is macOS-only). Signing happens before archiving and before the checksums job
# so the published tarball and its SHA-256 both cover the signed binary. The
# signing certificate is gated behind the release-signing environment and never
# leaves the runner. The "macOS Release Signing" section of AGENTS.md owns the
# permanent-identity invariant and the full signing contract.
build-darwin:
runs-on: macos-latest
environment: release-signing
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
strategy:
fail-fast: false
matrix:
include:
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build darwin binary
env:
GOOS: darwin
GOARCH: ${{ matrix.goarch }}
TAG: ${{ needs.release-please.outputs.tag_name }}
UMAMI_HOST: https://a.kunchenguid.com
UMAMI_WEBSITE_ID: f959e889-92f5-4121-8a1f-571b10861198
run: |
set -euo pipefail
mkdir -p dist
DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
COMMIT="$(git rev-parse --short=7 HEAD)"
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \
go build -ldflags "-X github.com/kunchenguid/no-mistakes/internal/buildinfo.Version=${TAG} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.Commit=${COMMIT} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.Date=${DATE} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.TelemetryHost=${UMAMI_HOST} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.TelemetryWebsiteID=${UMAMI_WEBSITE_ID}" \
-o "dist/no-mistakes" ./cmd/no-mistakes
# Import the Developer ID Application cert into an ephemeral keychain locked
# with a runtime-generated password, discover exactly one signing identity
# (fail closed otherwise), and sign with a fixed identifier, hardened runtime,
# and secure timestamp. The cert (CSC_LINK) is base64 and only ever touches
# a RUNNER_TEMP file that is deleted immediately after import.
- name: Import Developer ID certificate and sign
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
run: |
set -euo pipefail
TEAM_ID="9T2J7MNUP9"
if [ -z "${CSC_LINK:-}" ] || [ -z "${CSC_KEY_PASSWORD:-}" ]; then
echo "::error::CSC_LINK/CSC_KEY_PASSWORD signing secrets are missing; refusing to publish an unsigned macOS artifact" >&2
exit 1
fi
KEYCHAIN_PATH="$RUNNER_TEMP/nm-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 24)"
CERT_PATH="$RUNNER_TEMP/nm-developer-id.p12"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
# Reconstruct the cert from the base64 secret into RUNNER_TEMP only.
printf '%s' "$CSC_LINK" | tr -d '[:space:]' | openssl base64 -A -d > "$CERT_PATH"
# Ephemeral keychain: create, unlock, import scoped to codesign, and add
# to the search list so codesign can find the private key.
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$CERT_PATH" -P "$CSC_KEY_PASSWORD" -f pkcs12 \
-k "$KEYCHAIN_PATH" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
# Prepend the ephemeral keychain to the user search list, preserving the
# existing (space-free) entries. Word splitting here is intentional.
# shellcheck disable=SC2046
security list-keychains -d user -s "$KEYCHAIN_PATH" \
$(security list-keychains -d user | sed 's/["]//g')
rm -f "$CERT_PATH"
# Fail closed unless exactly one Developer ID Application identity for the
# expected Team ID is present.
IDENTITIES="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" \
| grep 'Developer ID Application' || true)"
COUNT="$(printf '%s\n' "$IDENTITIES" | grep -c 'Developer ID Application' || true)"
if [ "$COUNT" -ne 1 ]; then
echo "::error::expected exactly one Developer ID Application identity, found $COUNT" >&2
exit 1
fi
if ! printf '%s\n' "$IDENTITIES" | grep -q "($TEAM_ID)"; then
echo "::error::signing identity does not belong to Team ID $TEAM_ID" >&2
exit 1
fi
IDENTITY_HASH="$(printf '%s\n' "$IDENTITIES" | awk 'NR==1 {print $2}')"
codesign --force --timestamp --options runtime \
--identifier com.kunchenguid.no-mistakes \
--keychain "$KEYCHAIN_PATH" \
--sign "$IDENTITY_HASH" \
"dist/no-mistakes"
# Strict verification gate: any missing or ambiguous property fails the
# release before the artifact is archived or uploaded.
- name: Verify Developer ID signature
env:
GOARCH: ${{ matrix.goarch }}
run: |
set -euo pipefail
TEAM_ID="9T2J7MNUP9"
BIN="dist/no-mistakes"
case "$GOARCH" in
amd64) EXPECTED_ARCH="x86_64" ;;
arm64) EXPECTED_ARCH="arm64" ;;
*) echo "::error::unexpected GOARCH $GOARCH" >&2; exit 1 ;;
esac
# 1. Signature valid and strict.
codesign --verify --strict --verbose=2 "$BIN"
# 2. Developer ID identity + Team ID + permanent identifier, not ad-hoc.
SIG="$(codesign -dvvv "$BIN" 2>&1)"
printf '%s\n' "$SIG"
grep -q 'Authority=Developer ID Application' <<<"$SIG"
grep -q "TeamIdentifier=$TEAM_ID" <<<"$SIG"
grep -q 'Identifier=com.kunchenguid.no-mistakes' <<<"$SIG"
if grep -qi 'adhoc' <<<"$SIG"; then
echo "::error::signature is ad-hoc" >&2; exit 1
fi
# 3. Hardened runtime enabled.
if ! grep -Eq 'flags=0x[0-9a-f]+\([^)]*runtime[^)]*\)' <<<"$SIG"; then
echo "::error::hardened runtime flag not set" >&2; exit 1
fi
# 4. Secure (RFC-3161) timestamp present.
TIMESTAMP="$(sed -n 's/^Timestamp=//p' <<<"$SIG")"
if [[ -z "$TIMESTAMP" ]] || grep -qi '^none$' <<<"$TIMESTAMP"; then
echo "::error::secure timestamp is missing" >&2; exit 1
fi
# 5. Designated requirement is identity-based, never a content hash.
DR="$(codesign -d -r- "$BIN" 2>&1)"
printf '%s\n' "$DR"
grep -q 'anchor apple generic' <<<"$DR"
# The Team ID starts with a digit so codesign quotes it, but accept the
# unquoted form too so a codesign quirk cannot fail a valid release.
grep -Eq "leaf\[subject.OU][[:space:]]*=[[:space:]]*\"?$TEAM_ID\"?" <<<"$DR"
grep -q 'identifier "com.kunchenguid.no-mistakes"' <<<"$DR"
if grep -q 'cdhash H' <<<"$DR"; then
echo "::error::designated requirement is content-based (cdhash), not identity-based" >&2; exit 1
fi
# 6. Correct architecture for this matrix leg.
ARCHS="$(lipo -archs "$BIN")"
if [ "$ARCHS" != "$EXPECTED_ARCH" ]; then
echo "::error::architecture mismatch: got '$ARCHS', want '$EXPECTED_ARCH'" >&2; exit 1
fi
- name: Archive signed binary
env:
GOOS: darwin
GOARCH: ${{ matrix.goarch }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: |
set -euo pipefail
ARCHIVE="dist/no-mistakes-${TAG}-${GOOS}-${GOARCH}.tar.gz"
tar -C dist -czf "$ARCHIVE" no-mistakes
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: gh release upload "$TAG" "$ARCHIVE" --clobber
- uses: actions/upload-artifact@v7
with:
name: archive-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ env.ARCHIVE }}
# Tear down the ephemeral keychain on success and failure alike.
- name: Clean up signing keychain
if: always()
run: |
set -euo pipefail
if [ -n "${KEYCHAIN_PATH:-}" ]; then
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true
fi
rm -f "$RUNNER_TEMP/nm-developer-id.p12" 2>/dev/null || true
build-and-upload:
runs-on: ubuntu-latest
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: windows
goarch: amd64
- goos: windows
goarch: arm64
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Build archive
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
TAG: ${{ needs.release-please.outputs.tag_name }}
UMAMI_HOST: https://a.kunchenguid.com
UMAMI_WEBSITE_ID: f959e889-92f5-4121-8a1f-571b10861198
run: |
set -euo pipefail
mkdir -p dist
DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
COMMIT="$(git rev-parse --short=7 HEAD)"
BIN="no-mistakes"
OUT="dist/${BIN}"
if [ "$GOOS" = "windows" ]; then
BIN="${BIN}.exe"
OUT="dist/${BIN}"
fi
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" \
go build -ldflags "-X github.com/kunchenguid/no-mistakes/internal/buildinfo.Version=${TAG} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.Commit=${COMMIT} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.Date=${DATE} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.TelemetryHost=${UMAMI_HOST} -X github.com/kunchenguid/no-mistakes/internal/buildinfo.TelemetryWebsiteID=${UMAMI_WEBSITE_ID}" \
-o "$OUT" ./cmd/no-mistakes
if [ "$GOOS" = "windows" ]; then
ARCHIVE="dist/no-mistakes-${TAG}-${GOOS}-${GOARCH}.zip"
(
cd dist
zip -q "$(basename "$ARCHIVE")" "$BIN"
)
else
ARCHIVE="dist/no-mistakes-${TAG}-${GOOS}-${GOARCH}.tar.gz"
tar -C dist -czf "$ARCHIVE" "$BIN"
fi
echo "ARCHIVE=$ARCHIVE" >> "$GITHUB_ENV"
- name: Upload release asset
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: gh release upload "$TAG" "$ARCHIVE" --clobber
- uses: actions/upload-artifact@v7
with:
name: archive-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ env.ARCHIVE }}
checksums:
runs-on: ubuntu-latest
needs:
- release-please
- build-darwin
- build-and-upload
if: |
!cancelled() &&
needs.release-please.result == 'success' &&
needs.build-darwin.result == 'success' &&
needs.build-and-upload.result == 'success' &&
needs.release-please.outputs.release_created == 'true'
steps:
- uses: actions/download-artifact@v8
with:
path: dist
merge-multiple: true
- name: Generate checksums
run: |
set -euo pipefail
(
cd dist
sha256sum no-mistakes-* > ../checksums.txt
)
- name: Upload checksums
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: gh release upload "$TAG" checksums.txt --clobber
finalize:
runs-on: ubuntu-latest
needs:
- release-please
- build-darwin
- build-and-upload
- checksums
if: |
!cancelled() &&
needs.release-please.result == 'success' &&
needs.build-darwin.result == 'success' &&
needs.build-and-upload.result == 'success' &&
needs.checksums.result == 'success' &&
needs.release-please.outputs.release_created == 'true'
steps:
- name: Publish draft release as prerelease
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ needs.release-please.outputs.tag_name }}
run: gh release edit "$TAG" --draft=false --prerelease=true