diff --git a/.buildkite/build-linux.sh b/.buildkite/build-linux.sh index 5e41bd731..0ad594166 100755 --- a/.buildkite/build-linux.sh +++ b/.buildkite/build-linux.sh @@ -1,20 +1,35 @@ #!/usr/bin/env bash set -euo pipefail # Build the Linux vip-next binaries + checksums on a Buildkite Linux agent. -# Linux has no OS-enforced executable signature; we publish checksums (a detached -# GPG/cosign signature is optional — see the bottom of this file). +# Linux has no OS-enforced executable signature; we publish checksums. [ -f .buildkite/shared-pipeline-vars ] && . .buildkite/shared-pipeline-vars : "${BIN_BASE:=vip-next}" -command -v go >/dev/null 2>&1 || { echo "go not found; agent must provide Go ${GO_VERSION:-1.27}+" >&2; exit 1; } +if ! command -v go >/dev/null 2>&1; then + echo "--- :package: install go" + version="$(awk '/^toolchain / { print $2; exit }' go.mod)" + version="${version#go}" + [ -n "${version}" ] || { echo "no toolchain directive in go.mod" >&2; exit 1; } + case "$(uname -m)" in + x86_64|amd64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + *) echo "unsupported arch $(uname -m)" >&2; exit 1 ;; + esac + # Not `$HOME/go` — that is GOPATH. + prefix="${HOME}/.local" + mkdir -p "${prefix}" + curl -fsSL "https://go.dev/dl/go${version}.linux-${arch}.tar.gz" | tar -C "${prefix}" -xz + export PATH="${prefix}/go/bin:${PATH}" +fi go version -VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" +VERSION="$(go run -mod=mod ./cmd/stamp-version)" COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" build() { - local goarch="$1" out="dist/${BIN_BASE}-linux-${goarch}" + local goarch="$1" + local out="dist/${BIN_BASE}-linux-${goarch}" echo "--- :go: build linux/${goarch}" CGO_ENABLED=0 GOOS=linux GOARCH="${goarch}" \ go build -buildvcs=false -trimpath \ @@ -39,5 +54,4 @@ if [ -n "${native}" ]; then "dist/${BIN_BASE}-linux-${native}" whoami --help fi -# Optional detached signature (needs an infra-owned key); checksums-only by default. -# gpg --armor --detach-sign "dist/${BIN_BASE}-linux-amd64" # ← infra: enable if desired + diff --git a/.buildkite/build-macos.sh b/.buildkite/build-macos.sh index 77d1bf060..94687183e 100755 --- a/.buildkite/build-macos.sh +++ b/.buildkite/build-macos.sh @@ -1,12 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -# Build, and on tag builds sign + notarize, the macOS vip-next artifacts, on a -# Buildkite macOS agent (queue: mac): -# - two bare per-arch binaries: codesigned + notarized (online-verified; a bare -# Mach-O can't be stapled) -# - one universal .pkg installer: codesigned + productsigned + notarized + STAPLED -# (offline-verified) -# Checksums are written AFTER signing (signing changes the bytes). +# Build, sign, and notarize the macOS vip-next artifacts on a Buildkite macOS +# agent (queue: mac). Two bare per-arch binaries: codesigned + notarized +# (online-verified; a bare Mach-O can't be stapled). Checksums are written +# AFTER signing (signing changes the bytes). [ -f .buildkite/shared-pipeline-vars ] && . .buildkite/shared-pipeline-vars : "${BIN_BASE:=vip-next}" @@ -15,14 +12,22 @@ echo "--- :ruby: install gems" if command -v install_gems >/dev/null 2>&1; then install_gems; else bundle install; fi echo "--- :go: toolchain" -command -v go >/dev/null 2>&1 || { echo "go not found; agent must provide Go ${GO_VERSION:-1.27}+" >&2; exit 1; } +# Any Go will do: go.mod's `toolchain` directive makes it fetch go1.27.0 itself. +if ! command -v go >/dev/null 2>&1; then + echo "--- :package: install go" + brew install go +fi go version -VERSION="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" +# Bundler installs to `vendor/bundle`, which makes Go take `-mod=vendor`. +export GOFLAGS="${GOFLAGS:--mod=mod}" + +VERSION="$(go run -mod=mod ./cmd/stamp-version)" COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)" build() { - local goarch="$1" out="dist/${BIN_BASE}-darwin-${goarch}" + local goarch="$1" + local out="dist/${BIN_BASE}-darwin-${goarch}" echo "--- :go: build darwin/${goarch}" CGO_ENABLED=0 GOOS=darwin GOARCH="${goarch}" \ go build -buildvcs=false -trimpath \ @@ -48,50 +53,12 @@ if [ -n "${native}" ]; then "dist/${BIN_BASE}-darwin-${native}" whoami --help fi -if [ -z "${BUILDKITE_TAG:-}" ]; then - echo "--- not a tag build; skipping sign/notarize (unsigned checksums only)" - checksum "dist/${BIN_BASE}-darwin-arm64" - checksum "dist/${BIN_BASE}-darwin-amd64" - exit 0 -fi - echo "--- :closed_lock_with_key: fetch signing certs (fastlane match)" bundle exec fastlane configure_code_signing -# Build the universal binary from the UNSIGNED arches, then sign all three once. -uni="dist/${BIN_BASE}-darwin-universal" -lipo -create -output "${uni}" "dist/${BIN_BASE}-darwin-arm64" "dist/${BIN_BASE}-darwin-amd64" - -for bin in "dist/${BIN_BASE}-darwin-arm64" "dist/${BIN_BASE}-darwin-amd64" "${uni}"; do - echo "--- :closed_lock_with_key: codesign ${bin}" - codesign --remove-signature "${bin}" 2>/dev/null || true # drop Go's ad-hoc sig - codesign --sign "${MACOS_SIGN_IDENTITY}" --options runtime --timestamp --force "${bin}" - codesign --verify --strict --verbose=2 "${bin}" -done - -# Bare binaries: notarize (no staple — nothing to hold the ticket), then checksum. for arch in arm64 amd64; do bin="dist/${BIN_BASE}-darwin-${arch}" - echo "--- :cloud: notarize ${arch} (no staple)" - ditto -c -k --keepParent "${bin}" "${bin}.zip" - bundle exec fastlane notarize_artifact path:"${bin}.zip" - rm -f "${bin}.zip" + echo "--- :closed_lock_with_key: sign and notarize ${arch}" + bundle exec fastlane sign_and_notarize binary:"${bin}" checksum "${bin}" done - -# Universal .pkg: package the signed universal binary → sign the pkg → notarize + staple. -echo "--- :package: build + sign universal .pkg" -pkgroot="$(mktemp -d)" -cp "${uni}" "${pkgroot}/${BIN_BASE}" -pkg="dist/${BIN_BASE}-darwin-universal.pkg" -pkgbuild --root "${pkgroot}" --identifier com.automattic.vip-cli --version "${VERSION}" \ - --install-location /usr/local/bin "${pkg}.unsigned" -productsign --sign "${MACOS_INSTALLER_IDENTITY}" "${pkg}.unsigned" "${pkg}" -rm -f "${pkg}.unsigned" -rm -rf "${pkgroot}" - -echo "--- :cloud: notarize + staple .pkg" -bundle exec fastlane notarize_artifact path:"${pkg}" skip_stapling:false -xcrun stapler validate "${pkg}" -checksum "${uni}" -checksum "${pkg}" diff --git a/.buildkite/build-windows.ps1 b/.buildkite/build-windows.ps1 index c8d5865e7..de51043ef 100644 --- a/.buildkite/build-windows.ps1 +++ b/.buildkite/build-windows.ps1 @@ -1,6 +1,6 @@ #Requires -Version 5.1 -# Build vip-next.exe and (on tag builds) Authenticode-sign it, on a Buildkite -# Windows agent. Checksum is computed AFTER signing (signing changes the bytes). +# Build and Authenticode-sign vip-next.exe on a Buildkite Windows agent. +# Checksum is computed AFTER signing (signing changes the bytes). $ErrorActionPreference = 'Stop' $binBase = if ($env:BIN_BASE) { $env:BIN_BASE } else { 'vip-next' } @@ -9,12 +9,25 @@ function Get-GitOr($cmd, $fallback) { try { $v = & git @cmd 2>$null; if ($LASTEXITCODE -eq 0 -and $v) { return $v.Trim() } } catch {} return $fallback } -$version = Get-GitOr @('describe','--tags','--always','--dirty') 'dev' -$commit = Get-GitOr @('rev-parse','--short','HEAD') 'unknown' New-Item -ItemType Directory -Force -Path dist | Out-Null $out = "dist/$binBase-windows-amd64.exe" +# Any Go will do: go.mod's `toolchain` directive makes it fetch go1.27.0 itself. +if (-not (Get-Command go -ErrorAction SilentlyContinue)) { + Write-Host "--- :package: install go" + choco install golang -y --no-progress + if ($LASTEXITCODE -ne 0) { throw 'choco install golang failed' } + # choco updates the machine PATH, not this process's. + $env:PATH = "$env:PATH;$env:ProgramFiles\Go\bin" +} +go version +if ($LASTEXITCODE -ne 0) { throw 'go not usable after install' } + +$version = (& go run -mod=mod ./cmd/stamp-version | Out-String).Trim() +if ($LASTEXITCODE -ne 0 -or -not $version) { throw 'stamp-version failed' } +$commit = Get-GitOr @('rev-parse','--short','HEAD') 'unknown' + Write-Host "--- :go: build windows/amd64" $env:CGO_ENABLED = '0'; $env:GOOS = 'windows'; $env:GOARCH = 'amd64' $ldflags = "-s -w -X github.com/Automattic/vip/internal/version.Version=$version -X github.com/Automattic/vip/internal/version.Commit=$commit" @@ -25,28 +38,22 @@ Write-Host "--- :test_tube: smoke" & $out --version & $out whoami --help -if ($env:BUILDKITE_TAG) { - Write-Host "--- :closed_lock_with_key: Authenticode sign" - # ← infra: confirm the Windows cert mechanism. Draft = PFX-from-base64-secret, - # mirroring the current GitHub Actions workflow. EV certs can NOT use a plain - # PFX (FIPS-hardware since June 2023) — if you use Azure Trusted Signing, swap - # the two signtool lines for `signtool sign /fd SHA256 /tr /td SHA256 /dlib /dmdf $out`. - $pfxB64 = $env:WINDOWS_CERTIFICATE_PFX_BASE64 - $pfxPw = $env:WINDOWS_CERTIFICATE_PASSWORD - $ts = if ($env:WINDOWS_TIMESTAMP_URL) { $env:WINDOWS_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' } - if (-not $pfxB64 -or -not $pfxPw) { throw 'tag build but WINDOWS_CERTIFICATE_PFX_BASE64 / _PASSWORD not set' } - - $pfx = Join-Path $env:TEMP 'vip-codesign.pfx' - [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($pfxB64)) - try { - signtool sign /fd SHA256 /td SHA256 /tr $ts /f $pfx /p $pfxPw $out - if ($LASTEXITCODE -ne 0) { throw 'signtool sign failed' } - signtool verify /pa /v $out - if ($LASTEXITCODE -ne 0) { throw 'signtool verify failed' } - } finally { - Remove-Item $pfx -Force -ErrorAction SilentlyContinue - } -} +Write-Host "--- :closed_lock_with_key: Azure Trusted Signing" +$setupScript = (Get-Command setup_azure_trusted_signing.ps1 -ErrorAction Stop).Source +& $setupScript +if ($LASTEXITCODE -ne 0) { throw 'setup_azure_trusted_signing.ps1 failed' } + +Write-Host "--- :closed_lock_with_key: Authenticode sign" +& $env:SIGNTOOL_PATH sign /v ` + /fd $env:AZURE_FILE_DIGEST ` + /tr $env:AZURE_TIMESTAMP_SERVER ` + /td $env:AZURE_TIMESTAMP_DIGEST ` + /dlib $env:AZURE_CODE_SIGNING_DLIB ` + /dmdf $env:AZURE_METADATA_JSON ` + $out +if ($LASTEXITCODE -ne 0) { throw 'signtool sign failed' } +& $env:SIGNTOOL_PATH verify /pa /v $out +if ($LASTEXITCODE -ne 0) { throw 'signtool verify failed' } Write-Host "--- checksum" $hash = (Get-FileHash -Algorithm SHA256 $out).Hash.ToLower() diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 65cd37b1c..a3f5c9308 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -1,11 +1,12 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json --- -# Shared vars (CI_TOOLKIT_PLUGIN, GO_VERSION, BIN_BASE, signing identities) come -# from .buildkite/shared-pipeline-vars, which our setup source's before +# Shared vars (CI_TOOLKIT_PLUGIN, BIN_BASE) come from +# .buildkite/shared-pipeline-vars, which our setup source's before # `buildkite-agent pipeline upload` interpolates this file. # -# Every commit builds + smoke-tests all three platforms. Signing + notarization -# run only on TAG builds — gated on $BUILDKITE_TAG inside each build script. +# Every commit builds + smoke-tests all three platforms. macOS signs and +# notarizes, and Windows Authenticode-signs, on every build so those paths +# are not tag-only. env: DO_NOT_TRACK: '1' @@ -17,6 +18,8 @@ steps: - $CI_TOOLKIT_PLUGIN agents: queue: mac + env: + IMAGE_ID: $IMAGE_ID notify: - github_commit_status: context: 'Build & sign (macOS)' @@ -25,8 +28,10 @@ steps: - label: ':windows: Build & sign (Windows)' command: powershell -NoProfile -ExecutionPolicy Bypass -File .buildkite/build-windows.ps1 + plugins: + - $CI_TOOLKIT_PLUGIN agents: - queue: windows # ← infra: confirm Windows agent queue name + queue: windows notify: - github_commit_status: context: 'Build & sign (Windows)' @@ -36,7 +41,7 @@ steps: - label: ':linux: Build (Linux)' command: .buildkite/build-linux.sh agents: - queue: default # ← infra: confirm Linux agent queue name + queue: default notify: - github_commit_status: context: 'Build (Linux)' diff --git a/.buildkite/shared-pipeline-vars b/.buildkite/shared-pipeline-vars index c32333620..2af49705c 100644 --- a/.buildkite/shared-pipeline-vars +++ b/.buildkite/shared-pipeline-vars @@ -5,17 +5,12 @@ # a8c CI toolkit Buildkite plugin — provides install_gems, secret injection, and # GitHub status helpers on the macOS agent. -export CI_TOOLKIT_PLUGIN="automattic/a8c-ci-toolkit#5.3.1" # ← infra: confirm current version +export CI_TOOLKIT_PLUGIN="automattic/a8c-ci-toolkit#6.3.0" -# Go toolchain the build scripts require. encoding/json/v2 is standard in Go 1.27. -export GO_VERSION="1.27" # ← infra: match your agent provisioning - -# Binary base name + macOS signing identities (team PZYM8XX95Q = Automattic, Inc.). export BIN_BASE="vip-next" -export MACOS_TEAM_ID="PZYM8XX95Q" -export MACOS_SIGN_IDENTITY="Developer ID Application: Automattic, Inc. (PZYM8XX95Q)" -export MACOS_INSTALLER_IDENTITY="Developer ID Installer: Automattic, Inc. (PZYM8XX95Q)" # ← infra: confirm this cert exists -# NOTE: unlike the reference we set no Xcode IMAGE_ID / .xcode-version — this is a -# plain Go build, no fyne/Xcode. If your mac queue requires a specific VM image, -# add: export IMAGE_ID="" # ← infra +# The macOS VM to run on. The `mac` queue's command hook aborts when IMAGE_ID is +# unset, and the signing step needs `codesign` and `xcrun notarytool` +# regardless of Go being the compiler. +XCODE_VERSION=$(grep -Ev '^[[:space:]]*(#|$)' .xcode-version | head -n1 | sed -E 's/^[[:space:]]*//; s/^~> ?//; s/[[:space:]]*$//') +export IMAGE_ID="xcode-$XCODE_VERSION" diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 000000000..8515e56f3 --- /dev/null +++ b/.bundle/config @@ -0,0 +1,4 @@ +--- +BUNDLE_PATH: "vendor/bundle" +BUNDLE_JOBS: "3" +BUNDLE_RETRY: "3" diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 000000000..f5172c776 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,26 @@ +name: ShellCheck + +on: + pull_request: + push: + branches: + - develop + - trunk + workflow_dispatch: + +permissions: + contents: read + +jobs: + shellcheck: + name: Lint shell scripts + runs-on: ubuntu-latest + steps: + - name: Check out the source code + uses: actions/checkout@v7 + + # `-S warning` drops the `info` level, which is otherwise dominated by + # SC1091 for the `shared-pipeline-vars` file the build scripts source at + # runtime. ShellCheck ships preinstalled on the runner image. + - name: Run ShellCheck + run: git ls-files '*.sh' | xargs --no-run-if-empty shellcheck -S warning diff --git a/.gitignore b/.gitignore index 3d4aad8a2..04fcd4272 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,11 @@ go.work.sum # third_party/go-search-replace/MANIFEST (which IS tracked). Binaries stay out # of git so the repo does not carry ~19 MB of executables per upgrade. third_party/go-search-replace/*/ + +# Ruby tooling +vendor/bundle/ + +# Regenerated by fastlane on every run; `bundle exec fastlane lanes` prints the +# same lane summary as the README. +fastlane/README.md +fastlane/report.xml diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..7bcbb3808 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.9 diff --git a/.xcode-version b/.xcode-version new file mode 100644 index 000000000..7363977a2 --- /dev/null +++ b/.xcode-version @@ -0,0 +1 @@ +26.6 diff --git a/Gemfile b/Gemfile index 58cdbf207..cdac02e04 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,7 @@ source 'https://rubygems.org' -gem 'fastlane', '~> 2.237' -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.10' +gem 'fastlane', '~> 2.238' # Avoids "certificate verify failed (unable to get certificate CRL)" on some hosts. # See https://github.com/ruby/openssl/issues/949 gem 'openssl', '~> 4.0' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..8809815b1 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,374 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.8) + abbrev (0.1.2) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.4.0) + aws-partitions (1.1282.0) + aws-sdk-core (3.254.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal + jmespath (~> 1, >= 1.6.1) + logger + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.229.0) + aws-sdk-core (~> 3, >= 3.254.1) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.1.2) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + csv (3.3.6) + declarative (0.0.20) + digest-crc (0.7.0) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20260829) + dotenv (2.8.1) + emoji_regex (3.2.3) + erb (6.0.7) + excon (1.7.1) + logger + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-cookie_jar (0.0.8) + faraday (>= 0.8.0) + http-cookie (>= 1.0.0) + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) + faraday-multipart (1.2.0) + multipart-post (~> 2.0) + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) + fastimage (2.4.1) + fastlane (2.238.0) + CFPropertyList (>= 2.3, < 5.0.0) + abbrev (~> 0.1) + addressable (>= 2.9.0, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.197) + babosa (>= 1.0.3, < 2.0.0) + base64 (~> 0.2) + benchmark (>= 0.1.0) + bundler (>= 2.4.0, < 5.0.0) + colored (~> 1.2) + commander (~> 4.6) + csv (~> 3.3) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 2.0.0) + faraday (~> 2.7) + faraday-cookie_jar (~> 0.0.8) + faraday-follow_redirects (~> 0.3) + faraday-multipart (~> 1.0) + faraday-retry (~> 2.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.1.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + irb (>= 1.8) + json (< 3.0.0) + jwt (>= 2.10.3, < 4) + logger (>= 1.6, < 2.0) + mini_magick (>= 4.9.4, < 5.0.0) + multi_json (~> 1.12) + multipart-post (>= 2.0.0, < 3.0.0) + mutex_m (~> 0.3) + naturally (~> 2.2) + nkf (~> 0.2) + optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 4) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.107.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (1.2.5) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.11) + representable (~> 3.0) + retriable (>= 3.1, < 5.0) + google-apis-iamcredentials_v1 (0.28.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.18.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.66.0) + google-apis-core (>= 0.15.0, < 2.a) + google-cloud-core (1.9.0) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.2.2) + base64 (~> 0.2) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.7.0) + google-cloud-storage (1.62.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) + google-cloud-core (~> 1.6) + googleauth (~> 1.9) + mini_mime (~> 1.0) + google-logging-utils (0.2.0) + googleauth (1.17.4) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.2) + google-logging-utils (~> 0.1) + jwt (>= 1.4, < 4.0) + os (>= 0.9, < 2.0) + pstore (~> 0.1) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + io-console (0.9.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jmespath (1.6.2) + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.21.1) + multipart-post (2.4.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) + net-http (0.9.1) + uri (>= 0.11.1) + nkf (0.3.0) + openssl (4.0.2) + optparse (0.8.1) + os (1.1.4) + ostruct (0.6.3) + plist (3.7.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + pstore (0.2.1) + public_suffix (7.0.5) + rake (13.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.7.0) + io-console (~> 0.5) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (4.2.0) + rexml (3.4.4) + rouge (3.28.0) + rubyzip (2.4.1) + security (0.1.5) + signet (0.22.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 4.0) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + trailblazer-option (0.1.2) + tsort (0.2.0) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) + word_wrap (1.0.0) + xcodeproj (1.28.1) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + base64 + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + nkf + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + fastlane (~> 2.238) + openssl (~> 4.0) + +CHECKSUMS + CFPropertyList (3.0.8) sha256=2c99d0d980536d3d7ab252f7bd59ac8be50fbdd1ff487c98c949bb66bb114261 + abbrev (0.1.2) sha256=ad1b4eaaaed4cb722d5684d63949e4bde1d34f2a95e20db93aecfe7cbac74242 + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 + atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f + aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b + aws-partitions (1.1282.0) sha256=d6c7c4ad2e4f8cd4ca56445bfbb3958fec0deecd959c1e2bcd3a83c9aa308c18 + aws-sdk-core (3.254.1) sha256=518089e32134c3478cd4ec63d07fb966546a45e9e4cbf5f80d2cf16d5699d29b + aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749 + aws-sdk-s3 (1.229.0) sha256=7dd11a111875b3505d2ec0a0a383a6aab6c1badb3d4e94e7fbb958a24451f596 + aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 + babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac + claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e + colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c + colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a + commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 + declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 + digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 + domain_name (0.6.20260829) sha256=a9c1aa678423b9db3a4193b300fd76b9c21cb2a5f60d7dcf0a832b82d13764f0 + dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 + emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + excon (1.7.1) sha256=dd2d870eece1b5ce4e4f9efd938e67a69ccd8c003c3825b638b937e1082eaac2 + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb + faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad + faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe + fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 + fastlane (2.238.0) sha256=78e9252df2224c7e427012638e41051edfa29b133a40fda883fd2f1c13a77b98 + fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 + gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 + google-apis-androidpublisher_v3 (0.107.0) sha256=b8207eecc89e6a9f7ea11aa88a07bccdcae5ca90121330079db4ef4696a2f3ff + google-apis-core (1.2.5) sha256=e21562b5627a0fc6a0c3165e429c3afed0f6a628eaa514e83f7204dda1adff69 + google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 + google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 + google-apis-storage_v1 (0.66.0) sha256=fdf6d65d3fc77e7046b3494333a818ece9e2afd763bb161e1a7a9e70cf198351 + google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 + google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7 + google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 + google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 + google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b + googleauth (1.17.4) sha256=acc2cac2a6011048f149c922cbc6c9675719e165aa5000c7fa3876c480fe9ae3 + highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 + http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 + io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 + jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 + mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef + multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 + mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 + nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 + naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895 + openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2 + optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a + os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 + plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d + representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace + retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 + rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 + security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 + simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b + terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 + trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 + tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 + tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 + uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 + xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc + xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 + xcpretty-travis-formatter (1.0.1) sha256=aacc332f17cb7b2cba222994e2adc74223db88724fe76341483ad3098e232f93 + +BUNDLED WITH + 4.0.18 diff --git a/Makefile b/Makefile index 78ac8606f..4945fc2d5 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ GO ?= go GOFLAGS ?= LDFLAGS := -s -w \ - -X github.com/Automattic/vip/internal/version.Version=$(shell git describe --tags --always --dirty 2>/dev/null || echo dev) \ + -X github.com/Automattic/vip/internal/version.Version=$(shell GOFLAGS=-mod=mod $(GO) run ./cmd/stamp-version) \ -X github.com/Automattic/vip/internal/version.Commit=$(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) BIN_DIR := bin diff --git a/cmd/stamp-version/main.go b/cmd/stamp-version/main.go new file mode 100644 index 000000000..dc1883544 --- /dev/null +++ b/cmd/stamp-version/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/Automattic/vip/internal/version" +) + +func main() { + tag := strings.TrimSpace(os.Getenv("BUILDKITE_TAG")) + if tag == "" { + out, err := exec.Command("git", "describe", "--tags", "--exact-match").Output() + if err == nil { + tag = strings.TrimSpace(string(out)) + } + } + sha := "unknown" + if out, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output(); err == nil { + sha = strings.TrimSpace(string(out)) + } + fmt.Print(version.Stamp(tag, sha)) +} diff --git a/docs/BUILD-SIGNING.md b/docs/BUILD-SIGNING.md index f6cd33125..354d77047 100644 --- a/docs/BUILD-SIGNING.md +++ b/docs/BUILD-SIGNING.md @@ -230,44 +230,32 @@ Release builds run on **Buildkite** (Automattic's signing stack), not GitHub Actions. See `.buildkite/pipeline.yml` and the per-platform scripts. - **Pipeline:** `.buildkite/pipeline.yml` — three independent steps (macOS, - Windows, Linux), each building and signing on its own native agent. - `.buildkite/shared-pipeline-vars` is `source`'d before `buildkite-agent -pipeline upload` to supply shared values (toolkit plugin version, Go version, - signing identities). + Windows, Linux), each building on its own native agent. + Setup sources `.buildkite/shared-pipeline-vars` before pipeline upload + (CI toolkit plugin pin, `BIN_BASE`, `IMAGE_ID`). - **Build scripts:** `.buildkite/build-macos.sh`, `.buildkite/build-windows.ps1`, `.buildkite/build-linux.sh`. -- **Trigger / gating:** every commit builds + smoke-tests all three platforms; - **signing + notarization run only on tag builds** (gated on `$BUILDKITE_TAG` - inside each script), so notary quota and real certs aren't touched on PRs. -- **macOS certs:** fastlane `match` (`fastlane/Fastfile` → `configure_code_signing`), - `type: developer_id`, stored in S3 (`a8c-fastlane-match`), authenticated with an - App Store Connect API key. Two certs are needed: **Developer ID Application** - (signs the binaries) and **Developer ID Installer** (signs the `.pkg`). +- **Trigger / gating:** every commit builds and smoke-tests all three + platforms. macOS signs and notarizes, and Windows Authenticode-signs, on + every build. +- **macOS certs:** fastlane `match` (`fastlane/Fastfile` → + `configure_code_signing`), `type: developer_id`, stored in S3 + (`a8c-fastlane-match`). Signing and notarization are the + `sign_and_notarize` lane: Developer ID Application, identifier + `com.automattic.vip-cli`, no staple on a bare Mach-O. - **macOS artifacts:** two signed + notarized bare binaries (arm64, amd64; - online-verified) **plus** one signed + notarized + **stapled** universal `.pkg` - installer (offline-verified, installs `vip-next` to `/usr/local/bin`). -- **Windows / Linux artifacts:** signed `.exe` (Authenticode via `signtool`) and - the two Linux binaries with `.sha256` checksums. + online-verified). +- **Windows / Linux artifacts:** signed `.exe` (Azure Trusted Signing via + `setup_azure_trusted_signing.ps1`) and the two Linux binaries with + `.sha256` checksums. -### Secrets & values infra owns (`# ← infra:` in the files) +### Verifying a real run -Buildkite agent queues (`windows`/`default` names), the a8c-ci-toolkit plugin -version, the App Store Connect API key + `match` S3 credentials, the Developer ID -**Installer** certificate (net-new vs the reference — the `.pkg` half depends on -it), and the Windows certificate mechanism (PFX secret vs Azure Trusted Signing -vs hardware token — EV certs can no longer use a plain PFX). - -### Verifying a real run (infra manual gate) - -After the repo is registered in Buildkite, agents are provisioned, and secrets -are wired, run a **tag build** and confirm: +On a Buildkite build, confirm: - `notarytool` result **Accepted** for every submission. -- macOS binaries: `codesign --verify --strict --verbose=2` passes; - `spctl -a -t exec -vv ` assesses as accepted. -- macOS installer: `pkgutil --check-signature ` shows the Developer ID - Installer chain; `spctl -a -t install -vv ` accepts; `xcrun stapler -validate ` confirms the staple is present (offline). +- macOS binaries: `codesign --verify --strict --verbose=2` passes and + `codesign --display` shows `Identifier=com.automattic.vip-cli`. - Windows: `signtool verify /pa /v ` passes. - Every artifact has a matching `.sha256`. @@ -330,7 +318,7 @@ Upstream 0.0.11 publishes `darwin_{amd64,arm64}`, `linux_{386,amd64,arm64}`, `windows_{386,amd64,arm64}` — so `linux/arm64` (Graviton, ARM CI, Docker on Apple Silicon), previously unsupported, is covered with no self-building. -### ← infra: what changes in the signing pipeline +### Still needed in the signing pipeline 1. **Build agents need the `gh` CLI, authenticated**, for `make vendor-search-replace`. Alternatively pre-populate diff --git a/docs/CUTOVER-BREAKING-CHANGES.md b/docs/CUTOVER-BREAKING-CHANGES.md index 20a588599..405b077bc 100644 --- a/docs/CUTOVER-BREAKING-CHANGES.md +++ b/docs/CUTOVER-BREAKING-CHANGES.md @@ -250,7 +250,7 @@ here once rather than as 32 per-scenario `expected_drift` entries. - dev-env WordPress version validation unported — porting Node's naively would reject every valid version offline. - `make test-parity` build agents / `gh` auth for `make vendor-search-replace`, and the macOS - nested-binary signing step — see the `← infra:` items in `docs/BUILD-SIGNING.md`. + nested-binary signing step — see `docs/BUILD-SIGNING.md`. _Fixed since this list was written:_ the `--search-replace` double-apply (now 1.23), the unconditional table ANSI (`internal/output/table.go` now gates on `terminalTableIsTTY`, which is diff --git a/fastlane/Fastfile b/fastlane/Fastfile index dd0b6a2ea..145a586fe 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,67 +1,141 @@ # frozen_string_literal: true +require 'shellwords' +require 'fileutils' + UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? PROJECT_ROOT_FOLDER = File.dirname(File.expand_path(__dir__)) -# fastlane match cert storage (S3) — the shared a8c bucket. # ← infra: confirm +APPLE_TEAM_ID = 'PZYM8XX95Q' +SIGNING_IDENTITY = "Developer ID Application: Automattic, Inc. (#{APPLE_TEAM_ID})" + +# The signature's designated requirement embeds this identifier, and the Keychain ACL +# guarding the master key is bound to that requirement. `codesign` otherwise derives it +# from the file name (`vip-next-darwin-arm64`), so a rename would read as a different app. +SIGNING_IDENTIFIER = 'com.automattic.vip-cli' + CODE_SIGNING_STORAGE_OPTIONS = { storage_mode: 's3', s3_bucket: 'a8c-fastlane-match', s3_region: 'us-east-2' }.freeze -# app_store_connect_api_key reads these to build the ASC key. +CODE_SIGNING_ENV_VARS = %w[ + MATCH_S3_ACCESS_KEY + MATCH_S3_SECRET_ACCESS_KEY + MATCH_PASSWORD +].freeze + ASC_API_KEY_ENV_VARS = %w[ APP_STORE_CONNECT_API_KEY_KEY_ID APP_STORE_CONNECT_API_KEY_ISSUER_ID APP_STORE_CONNECT_API_KEY_KEY ].freeze -require_relative 'lib/env_manager' - -APP_IDENTIFIER = 'com.automattic.vip-cli' # ← infra: reuse team Dev ID cert vs a new match entry -TEAM_ID = 'PZYM8XX95Q' - before_all do - setup_ci # required for match to work in CI; harmless locally - EnvManager.set_up(env_file_name: 'vip-cli.env') - check_for_toolkit_updates unless is_ci || ENV['FASTLANE_SKIP_TOOLKIT_UPDATE_CHECK'] + # Ensures `match` works on CI by setting up a temporary keychain. No-op locally. + setup_ci end -# Places the Developer ID Application cert (and — see the infra note — the Installer -# cert) into the keychain so `codesign` / `productsign` can find them. +# Fetch the Developer ID Application certificate into the keychain. +# +# @param readonly [Boolean] Use `true` to only fetch the existing certificate from S3 via +# `match`. Use `false` to create or renew it on App Store Connect. +# lane :configure_code_signing do |readonly: true| - EnvManager.require_env_vars!(*ASC_API_KEY_ENV_VARS) - api_key = app_store_connect_api_key + require_env_vars!(*CODE_SIGNING_ENV_VARS) + require_env_vars!(*ASC_API_KEY_ENV_VARS) unless readonly - # Developer ID *Application* cert — signs the binaries (codesign). sync_code_signing( - app_identifier: APP_IDENTIFIER, platform: 'macos', type: 'developer_id', - api_key: api_key, team_id: TEAM_ID, readonly: readonly, + type: 'developer_id', + platform: 'macos', + team_id: APPLE_TEAM_ID, + # A CLI has no bundle to carry a provisioning profile, so there is no app identifier + # to key storage on. + app_identifier: [], + api_key: readonly ? nil : app_store_connect_api_key, + readonly: readonly, **CODE_SIGNING_STORAGE_OPTIONS ) - - # ← infra: ALSO provision the Developer ID *Installer* cert (for productsign on the - # .pkg). The reference doesn't need it. Depending on your match setup this is likely: - # sync_code_signing(app_identifier: APP_IDENTIFIER, platform: 'macos', type: 'developer_id', - # additional_cert_types: ['developer_id_installer'], api_key: api_key, - # team_id: TEAM_ID, readonly: readonly, **CODE_SIGNING_STORAGE_OPTIONS) - # Confirm the exact option/flow, then enable it. end -# Notarize a .zip (bare binary) or .pkg. Lane args arrive as strings, so an explicit -# `skip_stapling:false` is the ONLY thing that turns stapling on (bare binaries can't -# be stapled → default true; the .pkg passes false so it gets stapled → offline-verified). -lane :notarize_artifact do |options| - UI.user_error!('notarize_artifact requires path:') unless options[:path] - EnvManager.require_env_vars!(*ASC_API_KEY_ENV_VARS) +# Sign a built CLI binary with the Developer ID certificate and submit it for notarization. +# +# Run `configure_code_signing` first, or run this on a machine that already holds the +# certificate. +# +# @param binary [String] Path to the binary to sign, e.g. `dist/vip-next-darwin-arm64`. +# +lane :sign_and_notarize do |binary:| + binary = resolve_from_project_root(binary) + UI.user_error!("No binary at '#{binary}'") unless File.exist?(binary) + require_env_vars!(*ASC_API_KEY_ENV_VARS) + + sh( + 'codesign', + '--sign', SIGNING_IDENTITY, + '--identifier', SIGNING_IDENTIFIER, + # The hardened runtime is a precondition for notarization. + '--options', 'runtime', + '--timestamp', + '--force', + binary + ) + + verify_code_signing(binary: binary) + + # `notarytool` takes only .zip/.dmg/.pkg, and fastlane's `notarize` compresses `.app` + # bundles and nothing else, so a bare binary has to be archived here. + archive = "#{binary}.zip" + sh('ditto', '-c', '-k', '--keepParent', binary, archive) - skip_stapling = options[:skip_stapling].to_s != 'false' notarize( - package: options[:path], + package: archive, + # Only bundles and disk images can carry a stapled ticket. A bare binary relies on + # the online Gatekeeper check. + skip_stapling: true, api_key: app_store_connect_api_key, - skip_stapling: skip_stapling, print_log: true ) + + FileUtils.rm_f(archive) +end + +# Verify that a binary carries an intact signature from our Developer ID, so a wrongly +# signed build fails CI instead of shipping. +# +# Gatekeeper acceptance (`spctl --assess`) is deliberately not asserted: a bare binary +# cannot be stapled, so that check needs an online lookup that lags a fresh submission. +# +# @param binary [String] Path to the binary to verify. +# +lane :verify_code_signing do |binary:| + binary = resolve_from_project_root(binary) + + sh('codesign', '--verify', '--strict', '--verbose=2', binary) + + # `codesign --display` reports on stderr, which `sh` does not capture. + details = sh("codesign --display --verbose=2 #{binary.shellescape} 2>&1", log: false) + + unless details.include?("Authority=#{SIGNING_IDENTITY}") + UI.user_error!("#{binary} is not signed by '#{SIGNING_IDENTITY}':\n#{details}") + end + + unless details.include?("Identifier=#{SIGNING_IDENTIFIER}") + UI.user_error!("#{binary} has the wrong signing identifier, expected '#{SIGNING_IDENTIFIER}':\n#{details}") + end + + UI.success("#{binary} is signed by #{SIGNING_IDENTITY}") +end + +# fastlane runs lanes from the `fastlane/` directory, so a path a caller wrote +# relative to the repo root would otherwise resolve one level too deep. +def resolve_from_project_root(path) + File.absolute_path(path, PROJECT_ROOT_FOLDER) +end + +def require_env_vars!(*names) + missing = names.select { |name| ENV[name].to_s.empty? } + UI.user_error!("Missing required environment variable(s): #{missing.join(', ')}") unless missing.empty? end diff --git a/fastlane/lib/env_manager.rb b/fastlane/lib/env_manager.rb deleted file mode 100644 index c48d1bd41..000000000 --- a/fastlane/lib/env_manager.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -# Copied from Automattic/download (fastlane/lib/env_manager.rb). -# ← infra: replace with your canonical release-toolkit version if one exists. - -require 'dotenv' -require 'fastlane' - -# Manages loading of environment variables from a .env and accessing them in a user-friendly way. -class EnvManager - @env_path = nil - @env_example_path = nil - @print_error_lambda = nil - - def self.set_up( - env_file_name:, - env_file_folder: File.join(Dir.home, '.a8c-apps'), - example_env_file_path: 'fastlane/example.env', - print_error_lambda: ->(message) { FastlaneCore::UI.user_error!(message) } - ) - @env_path = File.join(env_file_folder, env_file_name) - @env_example_path = example_env_file_path - @print_error_lambda = print_error_lambda - - Dotenv.load(@env_path) - end - - def self.get_required_env!(key) - unless ENV.key?(key) - message = "Environment variable '#{key}' is not set." - - if running_on_ci? - @print_error_lambda.call(message) - elsif File.exist?(@env_path) - @print_error_lambda.call("#{message} Consider adding it to #{@env_path}.") - else - env_file_dir = File.dirname(@env_path) - env_file_name = File.basename(@env_path) - - @print_error_lambda.call <<~MSG - #{env_file_name} not found in #{env_file_dir} while looking for env var #{key}. - - Please copy #{@env_example_path} to #{@env_path} and fill in the value for #{key}. - - mkdir -p #{env_file_dir} && cp #{@env_example_path} #{@env_path} - MSG - end - end - - value = ENV.fetch(key) - FastlaneCore::UI.user_error!("Env var for key #{key} is set but empty. Please set a value for #{key}.") if value.to_s.empty? - value - end - - def self.require_env_vars!(*keys) - keys.each { |key| get_required_env!(key) } - end - - def self.running_on_ci? - ENV['CI'] == 'true' - end -end diff --git a/go.mod b/go.mod index f10b4fa99..d18ddb0c8 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/Automattic/vip go 1.27 +toolchain go1.27.0 + require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Khan/genqlient v0.8.1 diff --git a/internal/version/stamp.go b/internal/version/stamp.go new file mode 100644 index 000000000..793d75428 --- /dev/null +++ b/internal/version/stamp.go @@ -0,0 +1,15 @@ +package version + +import "strings" + +// Stamp is the Version ldflag. A 5.* git tag is a Go release; anything else +// (PRs, 4.x npm tags) is 5.0.0-dev.. +func Stamp(tag, shortCommit string) string { + if strings.HasPrefix(tag, "5.") { + return tag + } + if shortCommit == "" { + shortCommit = "unknown" + } + return "5.0.0-dev." + shortCommit +} diff --git a/internal/version/stamp_test.go b/internal/version/stamp_test.go new file mode 100644 index 000000000..830840231 --- /dev/null +++ b/internal/version/stamp_test.go @@ -0,0 +1,25 @@ +package version + +import "testing" + +func TestStamp(t *testing.T) { + t.Parallel() + + cases := []struct { + tag, sha, want string + }{ + {"5.0.0-beta1", "abc1234", "5.0.0-beta1"}, + {"5.0.0", "abc1234", "5.0.0"}, + {"5.1.0", "abc1234", "5.1.0"}, + {"4.1.1", "abc1234", "5.0.0-dev.abc1234"}, + {"", "abc1234", "5.0.0-dev.abc1234"}, + {"v5.0.0", "abc1234", "5.0.0-dev.abc1234"}, + {"", "", "5.0.0-dev.unknown"}, + } + for _, tc := range cases { + got := Stamp(tc.tag, tc.sha) + if got != tc.want { + t.Errorf("Stamp(%q, %q) = %q, want %q", tc.tag, tc.sha, got, tc.want) + } + } +}