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
35 changes: 20 additions & 15 deletions .github/actions/setup-hermetic/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ runs:
# renovate: depName=forkcloser/hermetic
HERMETIC_VERSION: v0.2.3
HERMETIC_CHECKSUMS_SHA256: c7eb09712dc3d756bead96ae1286b53de275134d0012601ef37f57ec793f9431
# The job's own token, for `gh attestation verify` only, here and not
# exported: a read of GitHub's attestations API, before any
# branch-declared binary runs.
# The job's own token, for gh alone (the release download and
# `gh attestation verify`), here and not exported: reads of GitHub's
# API, before any branch-declared binary runs.
GH_TOKEN: ${{ github.token }}
run: |
# Pin the root explicitly: hermetic's own default differs per
Expand Down Expand Up @@ -71,16 +71,23 @@ runs:
echo "hermetic ${HERMETIC_VERSION} already in the root (the cache image)"
else
tmp="$(mktemp -d)"
base="https://github.com/forkcloser/hermetic/releases/download/${HERMETIC_VERSION}"
# Retries for transient runner failures. Windows curl (schannel)
# refuses a handshake when the CA's revocation server is unreachable
# (exit 35, seen repeatedly): best-effort revocation still rejects a
# known-revoked certificate, and the checksums below are the real
# guard. schannel-only flag, so windows-only.
revoke=()
[ "$RUNNER_OS" != Windows ] || revoke=(--ssl-revoke-best-effort)
curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 3 --retry-all-errors "${revoke[@]}" \
-o "${tmp}/checksums.txt" "${base}/checksums.txt"
# Through the releases API with the job's token, by gh: the release
# page's download URL is an anonymous fetch, which GitHub throttles
# per runner IP under load (a 504 on every lane of a run, through
# curl's own retries), and a token sent there is routed, not checked.
# gh retries nothing itself; --clobber so a retry starts over. A
# pattern no asset matches is not an error to gh while another
# matches, so each file is checked for.
attempt=0
until gh release download "${HERMETIC_VERSION}" --repo forkcloser/hermetic --dir "${tmp}" --clobber \
--pattern checksums.txt --pattern "${asset}"; do
attempt=$((attempt + 1))
[ "$attempt" -lt 5 ] || { echo "::error::hermetic ${HERMETIC_VERSION}: checksums.txt or ${asset} could not be downloaded"; exit 1; }
sleep 3
done
for f in checksums.txt "${asset}"; do
[ -f "${tmp}/${f}" ] || { echo "::error::hermetic ${HERMETIC_VERSION} has no release asset ${f}"; exit 1; }
done
# No single digest tool exists everywhere: linux and windows git-bash
# ship coreutils sha256sum, macOS ships perl shasum. A mismatch names
# the computed digest and the size beside the expected one: a stale
Expand All @@ -101,8 +108,6 @@ runs:
echo "sha256 ok: $(basename "$file")"
}
verify_sha256 "${HERMETIC_CHECKSUMS_SHA256}" "${tmp}/checksums.txt"
curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 3 --retry-all-errors "${revoke[@]}" \
-o "${tmp}/${asset}" "${base}/${asset}"
expected="$(awk -v a="$asset" '$2 == a {print $1}' "${tmp}/checksums.txt")"
[ -n "$expected" ] || { echo "::error::checksums.txt of hermetic ${HERMETIC_VERSION} does not list ${asset}"; exit 1; }
verify_sha256 "${expected}" "${tmp}/${asset}"
Expand Down
56 changes: 28 additions & 28 deletions .hermetic/aqua-checksums.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"checksums": [
{
"id": "github_release/github.com/anchore/syft/v1.55.0/syft_1.55.0_darwin_arm64.tar.gz",
"checksum": "1217C258BD5EB91EF3189CF22A82E02F9CAC07E4CE934350F428E93AFF674625",
"id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_darwin_arm64.tar.gz",
"checksum": "17012A5A110323BB1069E513A8665F1AE445905C56B4E3F9A9DF4F95C5BB6161",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.55.0/syft_1.55.0_linux_amd64.tar.gz",
"checksum": "BB65249542DB5FEDEEB4D3F9B018B75E7264DBFBBB10486F044581A92F90490D",
"id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_linux_amd64.tar.gz",
"checksum": "9C077A7D43029B3B13E98A8DB720661E700C4D58DB6352153A06395AE4A2A704",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.55.0/syft_1.55.0_linux_arm64.tar.gz",
"checksum": "929BD163DB2D7CD2255D3BCB8967208079813D7B46B0C952F7B794D8E2C7D304",
"id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_linux_arm64.tar.gz",
"checksum": "410CBFA0AE0F63246BC0057843760B1DE2833CD23919E025E368FD7681308CDB",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/anchore/syft/v1.55.0/syft_1.55.0_windows_amd64.zip",
"checksum": "ADA4C9B93AF575F479CE80FCE6629897272A2D41F300A14BC49BACE020767CA2",
"id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_windows_amd64.zip",
"checksum": "06E9663E55F7F38FF966710670FBE0763182C621F7AC2F191BCB209A6DFBD969",
"algorithm": "sha256"
},
{
Expand Down Expand Up @@ -66,28 +66,28 @@
"algorithm": "sha256"
},
{
"id": "github_release/github.com/farcloser/limen/v0.24.0/limen_0.24.0_darwin_arm64.tar.gz",
"checksum": "BB7F8386488E585FE59F871CD3D5A4F74E8DA17B51118A11FE6FABA1C91E6FEB",
"id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_darwin_arm64.tar.gz",
"checksum": "6C013BBA485F3081BBA15342E7F6A9BB62B63E5D10D548DC2D425FACBE01CE87",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/farcloser/limen/v0.24.0/limen_0.24.0_linux_amd64.tar.gz",
"checksum": "8FD7C868E5C26E3E04C2F0F83D0904D271A9D7FD596E8D4303DC9689B8869209",
"id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_linux_amd64.tar.gz",
"checksum": "C054388FC8BD99D049C2D342FEC6422AAD181FFA4B1520D2E789E16A90D8C8F6",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/farcloser/limen/v0.24.0/limen_0.24.0_linux_arm64.tar.gz",
"checksum": "BDC58258E2B6280AFEF8AD7D8965D19B4AC3AB0C741C9FDC126403847F279E04",
"id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_linux_arm64.tar.gz",
"checksum": "AF2541BCCE26DFEEC8B62F802327E65EAC6C054D8647D251370E4072F668FB5A",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/farcloser/limen/v0.24.0/limen_0.24.0_windows_amd64.tar.gz",
"checksum": "9B862D79FCBB33676F2304FDA902A39F7623BB543EB43693F1F8015B2531730B",
"id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_windows_amd64.tar.gz",
"checksum": "53F06B63777631B57F08FBDE8BD466AA456286D54701DFD04553AF06306C14A0",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/farcloser/limen/v0.24.0/limen_0.24.0_windows_arm64.tar.gz",
"checksum": "05F751B5F5DB7FDB8544ACC055D9330A6CFDA64897FE31339B210A64C5C1CC49",
"id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_windows_arm64.tar.gz",
"checksum": "E83991B59F70E7DA0AB7A22FEAC93F6BF58768C683901C1B1D458BC4130E4879",
"algorithm": "sha256"
},
{
Expand Down Expand Up @@ -156,28 +156,28 @@
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.19.0/goreleaser_Darwin_all.tar.gz",
"checksum": "21ADCEFF6E676EF63497FC129983F8C454E5A46EC0125B905CBA0F83E7879722",
"id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Darwin_all.tar.gz",
"checksum": "B4F0702BCB2AAD05BD13AF5AC69EC94D84270DCE59B917E7A8C446C974E29372",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.19.0/goreleaser_Linux_arm64.tar.gz",
"checksum": "EF33741B684C9F62D6A21E7A3C8B051F139EA7B1D301693B67CC819C46226D53",
"id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Linux_arm64.tar.gz",
"checksum": "2DADA0A6BBCD10F3A05228D98E77D25FB362EFB9E5BAB6362CF37E64C8A26CB9",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.19.0/goreleaser_Linux_x86_64.tar.gz",
"checksum": "EBC3194AD1D1B1787614405285A0039665CD4C5BD3305215A1B201600F90630B",
"id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Linux_x86_64.tar.gz",
"checksum": "D2483159E6007F814132CB56635AD30A70013DBEB813D6033A7E01ADF6AE108C",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.19.0/goreleaser_Windows_arm64.zip",
"checksum": "FF7A728EBDF86770D45C522F88B901E7FB3C06692B0D0C6A8A3A2C9BCF01B682",
"id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Windows_arm64.zip",
"checksum": "B7AE30D558DB74D67BD03E39D3309AABF47E1A18649EF4AC4429F329C357F496",
"algorithm": "sha256"
},
{
"id": "github_release/github.com/goreleaser/goreleaser/v2.19.0/goreleaser_Windows_x86_64.zip",
"checksum": "67807490E909A37F58EF40B67601896D8D4DA2411C9839221DAE55732C243FE9",
"id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Windows_x86_64.zip",
"checksum": "3C6A58F6941224A25EC28DE733554743E9B2517C40C76991EEF5474F8C1C2C1A",
"algorithm": "sha256"
},
{
Expand Down
2 changes: 1 addition & 1 deletion .hermetic/hermetic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ registries:

packages:
# --- farcloser tools (local registry; standard once registered upstream) ---
- name: farcloser/limen@v0.24.0 # renovate: depName=farcloser/limen
- name: farcloser/limen@v0.26.0 # renovate: depName=farcloser/limen
registry: local
- name: golang/go@go1.27.3
- import: ../.limen/hermetic.yaml
4 changes: 2 additions & 2 deletions .limen/hermetic.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ packages:
- name: casey/just@1.59.0
- name: koalaman/shellcheck@v0.11.0
- name: lycheeverse/lychee@lychee-v0.24.2
- name: goreleaser/goreleaser@v2.19.0
- name: goreleaser/goreleaser@v2.20.0
- name: sigstore/cosign@v3.1.4
# The SBOM generator the release lane runs over every archive (book/releasing.md).
- name: anchore/syft@v1.55.0
- name: anchore/syft@v1.57.0
- name: jqlang/jq@jq-1.8.2
- name: cli/cli@v2.103.3
# --- spec tooling (local registry; see .limen/hermetic-registry.yaml) ---
Expand Down
49 changes: 39 additions & 10 deletions .limen/just/apple.just
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ apple-verify-notarized bundle:
echo "apple-verify-notarized: $bundle carries a valid stapled ticket"

# Submit the signed bundle to Apple and staple the ticket. The verdict is
# notarytool's own "status: Accepted": --wait's exit status is not it. Without
# an identity (a local build) there is nothing to notarize and the recipe says
# notarytool's own "status: Accepted", never an exit status. Without an
# identity (a local build) there is nothing to notarize and the recipe says
# so; with one and no credentials it fails, since a Developer ID build that
# leaves here unnotarized would be refused on every install.
[doc('Notarize a signed bundle with notarytool and staple the ticket')]
Expand All @@ -84,17 +84,46 @@ apple-notarize bundle:
rm -f "$archive"
ditto -c -k --sequesterRsrc --keepParent "$bundle" "$archive"
key=(--key "${APPLE_NOTARY_KEY:?}" --key-id "${APPLE_NOTARY_KEY_ID:?}" --issuer "${APPLE_NOTARY_ISSUER_ID:?}")
submission=$(xcrun notarytool submit "$archive" --wait "${key[@]}" 2>&1 | tee /dev/stderr) || true
# Submit, wait and the verdict are separate calls, each retried on its
# own: a connection that drops during the wait loses the verdict on an
# upload Apple already holds. Only a submit that exits 0 has finished its
# upload, so a failed one is submitted again; once one has, its id is
# waited on and asked about, never submitted again.
for attempt in 1 2 3; do
submission=$(xcrun notarytool submit "$archive" --no-wait "${key[@]}" 2>&1 | tee /dev/stderr) && break
[ "$attempt" -lt 3 ] || { echo "apple-notarize: $archive could not be submitted" >&2; exit 1; }
sleep $((attempt * 15))
done
id=$(printf '%s\n' "$submission" | sed -n 's/^ *id: \([0-9a-f-]*\)$/\1/p' | head -n 1)
if ! printf '%s\n' "$submission" | grep -q '^ *status: Accepted$'; then
echo "apple-notarize: Apple did not accept the submission${id:+ $id}" >&2
[ -z "$id" ] || xcrun notarytool log "$id" "${key[@]}" >&2 || true
[ -n "$id" ] || { echo "apple-notarize: notarytool submit returned no submission id" >&2; exit 1; }
# A "status:" line is the end of processing, whatever wait exits with;
# anything else is a wait cut short, resumed on the same id.
for attempt in 1 2 3 4 5; do
waited=$(xcrun notarytool wait "$id" "${key[@]}" 2>&1 | tee /dev/stderr) && break
printf '%s\n' "$waited" | grep -q '^ *status: ' && break
[ "$attempt" -lt 5 ] || { echo "apple-notarize: submission $id did not finish processing" >&2; exit 1; }
sleep $((attempt * 15))
done
for attempt in 1 2 3; do
info=$(xcrun notarytool info "$id" "${key[@]}" 2>&1 | tee /dev/stderr) && break
[ "$attempt" -lt 3 ] || { echo "apple-notarize: the status of submission $id could not be read" >&2; exit 1; }
sleep $((attempt * 10))
done
if ! printf '%s\n' "$info" | grep -q '^ *status: Accepted$'; then
echo "apple-notarize: Apple did not accept the submission $id" >&2
xcrun notarytool log "$id" "${key[@]}" >&2 || true
exit 1
fi
# staple validates what it writes; a second validate only asks the
# network again.
xcrun stapler staple "$bundle"
echo "apple-notarize: $bundle notarized (submission ${id:-?}), ticket stapled"
# staple fetches the ticket from Apple's ticket service and validates
# what it writes: a connection that drops on the runner fails it with the
# submission accepted, so it is tried five times, further apart each time,
# before the release stops.
for attempt in 1 2 3 4 5; do
xcrun stapler staple "$bundle" && break
[ "$attempt" -lt 5 ] || { echo "apple-notarize: the ticket for submission $id could not be stapled" >&2; exit 1; }
sleep $((attempt * 15))
done
echo "apple-notarize: $bundle notarized (submission $id), ticket stapled"

# ditto, not zip: the archiver that keeps a bundle's extended attributes and
# code signature intact, so what a user unzips is what was signed.
Expand Down
46 changes: 43 additions & 3 deletions .limen/just/release.just
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,13 @@ release *args:
}
# A fourth segment is a fork's rebuild of the same upstream version
# (v10.6.1.2, book/forks.md): never a -suffix, which semver sorts before
# the release it rebuilds.
# the release it rebuilds. A rebuild carries no prerelease of its own:
# goreleaser reads no semver from it (--ci below), so a suffix would be
# published as a full release.
rebuild_tag_re='^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'
require_version_tag() {
if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'; then
if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$' &&
! printf '%s' "$1" | grep -qE "$rebuild_tag_re"; then
echo "'$1' is not a version tag (vX.Y.Z, vX.Y.Z-prerelease, or a fork's rebuild vX.Y.Z.N)." >&2
exit 2
fi
Expand Down Expand Up @@ -154,7 +158,22 @@ release *args:
echo "HEAD carries no tag — this lane only runs on a tag push." >&2
exit 1
fi
exec goreleaser release --config .release-go.yaml --clean
# goreleaser refuses a tag that is not semver, and a fork's
# rebuild tag is not: --skip=validate is its one way past, and it
# also drops goreleaser's dirty-tree and tag-at-HEAD checks, so
# the two are made here first, on the tag this run was pushed for.
opts=()
tag="${GITHUB_REF_NAME:-}"
if printf '%s' "$tag" | grep -qE "$rebuild_tag_re"; then
require_clean
if ! git describe --exact-match --tags --match "$tag" HEAD >/dev/null 2>&1; then
echo "$tag is not on HEAD — this lane releases the tag its run was pushed for." >&2
exit 1
fi
export GORELEASER_CURRENT_TAG="$tag"
opts+=(--skip=validate)
fi
exec goreleaser release --config .release-go.yaml --clean ${opts[@]+"${opts[@]}"}
;;
--dry-run)
echo "dry runs are a local concern: use 'just do release --local --dry-run'." >&2
Expand Down Expand Up @@ -218,3 +237,24 @@ release *args:
fi
;;
esac

# goreleaser's sign command (.release-go.yaml `signs:`): one keyless
# signature over a release file, in CI. cosign gives up on Fulcio at the first
# dropped connection and retries Rekor once, so a reset on the runner fails
# the release with nothing wrong in it. Every attempt is a fresh key, a fresh
# certificate and a new log entry, so one that failed halfway cannot collide
# with the next; its partial bundle is removed first.
[doc("Sign a release file keyless with cosign into a Sigstore bundle (goreleaser's sign command)")]
[positional-arguments]
release-sign file bundle:
#!/usr/bin/env bash
set -euo pipefail
file="$1" bundle="$2"
for attempt in 1 2 3; do
rm -f "$bundle"
if cosign sign-blob --yes --bundle "$bundle" "$file" && [ -s "$bundle" ]; then
exit 0
fi
[ "$attempt" -lt 3 ] || { echo "release-sign: $file could not be signed" >&2; exit 1; }
sleep $((attempt * 15))
done
Loading
Loading