Speedtests through a proxy, containers that never open on a guess, ba… #134
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| push: | |
| tags: ['v*'] | |
| # Manual escape hatch for a stranded tag: the 2026-08-06 Actions outage | |
| # throttled webhooks and the TAG event never replayed (push events did, tag | |
| # events do not), leaving a pushed tag with no release run - the remedy was | |
| # deleting and re-pushing the tag. With this, dispatch the workflow AT the | |
| # tag ref instead (Actions UI -> Release -> Run workflow -> pick the tag). | |
| workflow_dispatch: | |
| # Only one release run per tag at a time. Re-cutting a tag (delete + re-push, as | |
| # the rc loop does) or a manual re-dispatch QUEUES behind the in-flight run | |
| # rather than cancelling it. Serializing still prevents the concurrent asset- | |
| # upload collision the cancel was added for (422 already_exists / http2 | |
| # REFUSED_STREAM) - two runs never upload at once - WITHOUT the hazard cancel | |
| # introduced: a workflow_dispatch (or a late-delivered throttled tag webhook) | |
| # sharing this group could otherwise cancel a run mid-publish. Releases are | |
| # draft-until-complete, so a cancelled run strands only an invisible draft that | |
| # a re-run resumes - but for stable it can also leave the brew cask pointing at | |
| # download URLs that stay 404 until that re-run completes and flips the release | |
| # public. The cost is only that an rc re-cut waits for the previous run to | |
| # finish instead of aborting it - cheap next to a dangling stable window. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| # Least privilege by default; the publish job widens its own grant below. | |
| permissions: | |
| contents: read | |
| jobs: | |
| guard: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # workflow_dispatch (the stranded-tag escape hatch) can fire on a BRANCH, | |
| # and a branch may be NAMED like a version - the SemVer check below reads | |
| # GITHUB_REF_NAME and would pass it, while GoReleaser publishes the git tag | |
| # at HEAD instead. On a v-named branch whose HEAD sits on an existing | |
| # release tag that would OVERWRITE a shipped release. Refuse anything that | |
| # is not a tag ref outright, before any checkout or token use. | |
| - name: Require a tag ref | |
| run: | | |
| if [ "${GITHUB_REF_TYPE}" != "tag" ]; then | |
| echo "::error::release runs only on a tag ref (got ${GITHUB_REF_TYPE} '${GITHUB_REF_NAME}'); dispatch the workflow with a tag ref, not a branch" | |
| exit 1 | |
| fi | |
| # The trigger glob 'v*' also matches junk like 'v', 'vtest', or 'v1.2' - a | |
| # stray or malformed tag must not cut a release. Enforce a proper SemVer | |
| # grammar (leading v) before any checkout or token use. This is a regex, not a | |
| # full parser, but it is a real SemVer grammar: it rejects the shapes that | |
| # actually slip through a naive pattern - leading-zero numeric core/prerelease | |
| # identifiers (v01.2.3, v1.2.3-01) and empty dot identifiers (v1.2.3-a..b) - | |
| # while GoReleaser stays the authoritative validator downstream. | |
| - name: Guard SemVer tag | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$' | |
| if ! printf '%s' "$tag" | grep -Eq "$semver"; then | |
| echo "::error::tag '$tag' is not a valid SemVer tag (vMAJOR.MINOR.PATCH[-prerelease]); refusing to release" | |
| exit 1 | |
| fi | |
| # SemVer permits +build metadata, but this pipeline CANNOT ship it | |
| # safely: the prerelease test below is a shell `*-*` glob, and build | |
| # metadata like +build-1 contains a hyphen, so a STABLE tag with build | |
| # metadata would be misread as a prerelease and skip the immutable- | |
| # stable overwrite guard. Docker tags reject '+' outright as well. | |
| # Reject build metadata here rather than silently mishandle it. | |
| case "$tag" in | |
| *+*) | |
| echo "::error::tag '$tag' carries +build metadata, which this release pipeline does not support (it breaks the stable/prerelease split and Docker tagging); cut vMAJOR.MINOR.PATCH[-prerelease] instead" | |
| exit 1 | |
| ;; | |
| esac | |
| # Stable releases are immutable ONCE PUBLISHED: their checksums are attested | |
| # in the publish job, so silently overwriting a published stable asset would | |
| # invalidate that provenance. .goreleaser.yaml keeps | |
| # replace_existing_artifacts: true for the rc re-cut loop (a plain bool | |
| # there, not templateable per channel), so this guard confines that | |
| # overwrite to prerelease tags - it fails the run if a STABLE tag's GitHub | |
| # release is already published. Cut a new version instead. | |
| # | |
| # Published, not merely existing: releases are created as DRAFTS and only | |
| # flipped public by the publish job's final step, after every attestation | |
| # succeeds, so a stranded draft from an incomplete run must be RESUMABLE. | |
| # It is, by the API's own semantics: "get a release by tag name" returns | |
| # "a published release with the specified tag" (GitHub REST docs), so a | |
| # draft answers HTTP 404 here and the re-run replaces it wholesale via | |
| # release.replace_existing_draft. | |
| # | |
| # FAIL CLOSED: `gh release view` (and `gh api`) exit non-zero for a genuine | |
| # 404 AND for auth failures, rate limiting, and outages alike, so treating | |
| # every non-zero exit as "absent" would let a transient API blip green-light | |
| # an overwrite. Only a definite HTTP 404 may proceed; a 200 refuses; any | |
| # other result retries and then fails closed. The workflow_dispatch escape | |
| # hatch exists for the very outages that cause those transient errors, so | |
| # this correlation is real, not hypothetical. | |
| - name: Forbid re-cutting a published stable release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| case "$tag" in | |
| *-*) echo "prerelease tag ($tag): re-cut permitted"; exit 0 ;; | |
| esac | |
| for attempt in 1 2 3; do | |
| if out=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" 2>&1); then | |
| echo "::error::stable release '$tag' is already published; refusing to overwrite published, attested assets. Cut a new version instead." | |
| exit 1 | |
| fi | |
| case "$out" in | |
| *"HTTP 404"*) echo "stable tag ($tag): no published release - ok to publish (a stranded draft, if any, is replaced and resumed)"; exit 0 ;; | |
| esac | |
| echo "attempt $attempt: could not determine release state for '$tag' (not a 404): $out" | |
| sleep 5 | |
| done | |
| echo "::error::could not confirm whether stable release '$tag' already exists (API errors, not a 404); failing closed. Re-run once the API is healthy." | |
| exit 1 | |
| # Gate the publish on the FULL exact-SHA CI (race, vet, gofmt, UI, govulncheck, | |
| # cross-build, native smoke, goreleaser check) so a tag can never publish code the | |
| # regular CI would have failed. The reusable workflow checks out this same commit. | |
| ci: | |
| needs: [guard] | |
| uses: ./.github/workflows/ci.yml | |
| release: | |
| needs: [ci] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # create the GitHub Release + upload assets | |
| packages: write # push the GHCR image | |
| id-token: write # mint the short-lived OIDC token the attestations are signed with | |
| attestations: write # write the build-provenance attestations to the attestation store | |
| steps: | |
| - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | |
| with: | |
| fetch-depth: 0 # full history so goreleaser can build the changelog | |
| # Don't leave the default GITHUB_TOKEN persisted in .git/config: goreleaser | |
| # uses its own scoped tokens below, so nothing here needs the credential, | |
| # and a persisted one is a needless secret at rest in the workspace. | |
| persist-credentials: false | |
| - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 | |
| with: | |
| go-version-file: go.mod | |
| # GoReleaser does NOT resolve the current tag with `git describe`: it reads | |
| # GORELEASER_CURRENT_TAG first (pinned on the Run GoReleaser step below to | |
| # the guarded ref), else the HIGHEST version tag pointing at HEAD. The env | |
| # pin is what guarantees the publish uses the exact tag the guards | |
| # approved; this assert is the belt-and-suspenders check that the | |
| # checked-out HEAD actually carries that tag. Membership, not equality: a | |
| # commit may legitimately carry several tags (the rc-to-stable promote | |
| # cuts the stable tag on the same SHA as the final rc), so require | |
| # GITHUB_REF_NAME to be AMONG the tags pointing at HEAD. | |
| - name: Assert the pushed tag points at HEAD | |
| run: | | |
| if ! git tag --points-at HEAD | grep -Fxq -- "${GITHUB_REF_NAME}"; then | |
| echo "::error::tag '${GITHUB_REF_NAME}' does not point at HEAD (tags at HEAD: $(git tag --points-at HEAD | paste -sd ' ' - || true)); the release would publish a different commit than was guarded" | |
| exit 1 | |
| fi | |
| # Re-run the stable-overwrite guard immediately before publishing. The guard | |
| # JOB ran separately with the full CI run in between, and "Re-run failed | |
| # jobs" skips an already-passed guard - so a transient API blip that let the | |
| # guard pass must not carry through to the actual overwrite. Same fail-closed | |
| # logic as the guard job (kept inline because the guard job deliberately runs | |
| # before any checkout, so the two cannot share a script without weakening | |
| # it), and the same published-only semantics: a stranded DRAFT answers 404 | |
| # here and is resumed, only a PUBLISHED stable release blocks. | |
| - name: Recheck no published stable release before publishing | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| case "$tag" in | |
| *-*) echo "prerelease tag ($tag): re-cut permitted"; exit 0 ;; | |
| esac | |
| for attempt in 1 2 3; do | |
| if out=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" 2>&1); then | |
| echo "::error::stable release '$tag' is already published; refusing to overwrite published, attested assets." | |
| exit 1 | |
| fi | |
| case "$out" in | |
| *"HTTP 404"*) echo "stable tag ($tag): still no published release - ok to publish (a stranded draft, if any, is replaced and resumed)"; exit 0 ;; | |
| esac | |
| echo "attempt $attempt: could not determine release state for '$tag' (not a 404): $out" | |
| sleep 5 | |
| done | |
| echo "::error::could not confirm whether stable release '$tag' already exists (API errors, not a 404); failing closed." | |
| exit 1 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 | |
| # Re-cutting an rc replaces the assets of a release that is already PUBLIC | |
| # (replace_existing_artifacts, the amend loop this repo actually uses), so | |
| # for the length of that run the rc's download URLs served bytes whose new | |
| # digests nothing had attested yet - the draft-until-complete flow covered | |
| # the first cut of a tag and not its second. Putting the release back into | |
| # draft first restores the property for re-cuts too: the tag goes dark | |
| # while it is incomplete, and the same final step that publishes a first | |
| # cut publishes this one. A tag with no published release yet is untouched | |
| # (the normal case), and a stable tag never reaches here - its own guard | |
| # refuses a re-cut outright. | |
| - name: Re-draft a published prerelease before replacing its bytes | |
| if: ${{ contains(github.ref_name, '-') }} | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| # Only a PUBLISHED release answers here; a draft is already invisible | |
| # (the tags endpoint returns published releases only), so it needs no | |
| # action and a definite 404 is the first-cut case. | |
| # | |
| # Same fail-closed discipline as the stable guard: `gh api` exits | |
| # non-zero for auth failures, rate limits and outages as well as for a | |
| # genuine 404, so treating every error as "absent" would skip the | |
| # re-draft exactly when the API is unhealthy - and then replace a live | |
| # release's bytes anyway. Only a definite 404 may proceed. | |
| if out=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${tag}" 2>&1); then | |
| : | |
| else | |
| case "$out" in | |
| *"HTTP 404"*) | |
| echo "no published release for '$tag' - nothing to re-draft" | |
| exit 0 ;; | |
| *) | |
| echo "::error::could not determine whether '$tag' is already published ($out); refusing to replace its assets blind" | |
| exit 1 ;; | |
| esac | |
| fi | |
| echo "'$tag' is already published; putting it back to draft while its assets are replaced" | |
| for attempt in 1 2 3; do | |
| if gh release edit "$tag" --draft=true --repo "${GITHUB_REPOSITORY}"; then | |
| exit 0 | |
| fi | |
| echo "attempt $attempt: could not re-draft '$tag'" | |
| sleep 5 | |
| done | |
| # Fail closed: continuing would publish replacement bytes into a live | |
| # release with no attestation, which is the thing this step exists to | |
| # prevent. | |
| echo "::error::could not put '$tag' back to draft before replacing its assets; refusing to overwrite a published prerelease in place" | |
| exit 1 | |
| - name: Run GoReleaser | |
| uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 | |
| with: | |
| # Pinned to an EXACT version (not '~> v2') so a new goreleaser release | |
| # can't silently change tag-push behaviour; bump it deliberately, in step | |
| # with the pin in ci.yml's goreleaser-check job. | |
| version: 'v2.17.0' | |
| args: release --clean | |
| env: | |
| # Pin GoReleaser's current-tag resolution to the exact ref the guards | |
| # validated. Without this it resolves the highest version tag at HEAD, | |
| # which diverges from the guarded ref when a commit carries more than | |
| # one tag - letting it publish/overwrite a DIFFERENT release than was | |
| # checked. This is the real guarantee behind the assert step above. | |
| GORELEASER_CURRENT_TAG: ${{ github.ref_name }} | |
| # Creates the GitHub Release as a DRAFT (release.draft in | |
| # .goreleaser.yaml; the final step below flips it public once the | |
| # attestations are in) and pushes the GHCR image. | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Publishing the Homebrew tap and winget manifests writes to OTHER | |
| # repos, which GITHUB_TOKEN cannot reach. Provide a token for that, and | |
| # keep it least-privilege: a FINE-GRAINED PAT scoped to ONLY the tap and | |
| # winget repos, "Contents: read and write" and nothing else, with the | |
| # SHORTEST practical expiry (rotate on expiry). A classic/broadly-scoped | |
| # PAT here would let a compromised release step write to every repo the | |
| # owner can reach. goreleaser reads it as its release token. | |
| TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} | |
| # Provenance for every downloadable artifact: subject-checksums attests each | |
| # file listed in checksums.txt (the .tar.gz/.zip archives and the .deb/.rpm | |
| # packages), so a consumer can run `gh attestation verify <file> --repo ...` | |
| # and prove it was built by THIS workflow from THIS commit. Signed with the | |
| # OIDC id-token above - no long-lived key at rest. The GHCR images get the | |
| # same treatment below, keyed on their pushed manifest digests. | |
| - name: Attest build provenance (release artifacts) | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-checksums: dist/checksums.txt | |
| # Image provenance needs the digest of what was actually PUSHED - the | |
| # multi-arch index digest is minted by the registry, so it can only be read | |
| # back, never precomputed. Resolve both variants' VERSION tags ({{.Version}} | |
| # is the tag minus the leading v, so this works on prerelease and stable | |
| # alike). Only those: the floating tags still point at the previous release | |
| # at this point in the run, and are moved onto these digests further down, | |
| # after the attestations below have succeeded. | |
| # Fail closed on anything that does not parse as a digest: publishing an | |
| # image without its attestation would be silent, and silent is the failure | |
| # mode this step exists to remove. | |
| - name: Resolve pushed image digests | |
| id: image_digests | |
| run: | | |
| version="${GITHUB_REF_NAME#v}" | |
| resolve() { # resolve <output-name> <image-ref> | |
| digest=$(docker buildx imagetools inspect "$2" --format '{{json .Manifest.Digest}}' | tr -d '"') | |
| case "$digest" in | |
| sha256:*) echo "$2 -> $digest"; echo "$1=$digest" >> "$GITHUB_OUTPUT" ;; | |
| *) echo "::error::could not resolve the pushed digest for $2 (got '$digest'); refusing to skip its attestation"; exit 1 ;; | |
| esac | |
| } | |
| resolve default "ghcr.io/pingular/pingularity:${version}" | |
| resolve iperf "ghcr.io/pingular/pingularity:${version}-iperf" | |
| # subject-name + subject-digest is the image form the attestation store | |
| # keys on, so `gh attestation verify oci://ghcr.io/pingular/pingularity:<tag> | |
| # --repo pingular/pingularity` works for pulls. The action takes the BARE | |
| # image name - the oci:// scheme belongs only to the verify CLI's syntax, | |
| # and the action rejects it as an invalid image name. push-to-registry also | |
| # attaches the attestation to GHCR itself (the login above is still live), | |
| # so registry-side policy tooling can find it without the GitHub API. | |
| - name: Attest build provenance (default image) | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-name: ghcr.io/pingular/pingularity | |
| subject-digest: ${{ steps.image_digests.outputs.default }} | |
| push-to-registry: true | |
| - name: Attest build provenance (iperf image) | |
| uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 | |
| with: | |
| subject-name: ghcr.io/pingular/pingularity | |
| subject-digest: ${{ steps.image_digests.outputs.iperf }} | |
| push-to-registry: true | |
| # Promote the floating tags LAST, and only for a stable release. GoReleaser | |
| # pushes the immutable version tags; `latest` and `latest-iperf` are moved | |
| # here, by digest, once every attestation above has succeeded - so the | |
| # channel `docker pull` serves by default can never point at an image whose | |
| # provenance does not exist yet. A run that dies at an attestation leaves | |
| # latest exactly where it was, on the previous release, which is the safe | |
| # place for it to be. imagetools create re-points a tag at an existing | |
| # multi-arch index without rebuilding or re-pushing any layer. | |
| - name: Promote the floating tags (stable only) | |
| if: ${{ !contains(github.ref_name, '-') }} | |
| env: | |
| GORELEASER_CURRENT_TAG: ${{ github.ref_name }} | |
| run: | | |
| version="${GORELEASER_CURRENT_TAG#v}" | |
| repo="ghcr.io/pingular/pingularity" | |
| for pair in "latest:${version}" "latest-iperf:${version}-iperf"; do | |
| floating="${pair%%:*}" | |
| pinned="${pair#*:}" | |
| echo "promoting ${floating} -> ${pinned}" | |
| docker buildx imagetools create --tag "${repo}:${floating}" "${repo}:${pinned}" | |
| done | |
| # Prove the move landed on the digest we just attested. Read each digest | |
| # through a helper that FAILS on anything but a real one: comparing the | |
| # raw command output would pass when BOTH reads fail, since two empty | |
| # strings are equal - a verification step that cannot fail is worse than | |
| # none, because it is quoted as evidence. The pipeline's exit status is | |
| # `tr`'s, so `set -e` does not catch it either. | |
| digest_of() { | |
| ref="$1" | |
| if ! out=$(docker buildx imagetools inspect "$ref" --format '{{json .Manifest.Digest}}' 2>&1); then | |
| echo "::error::could not read the digest of $ref: $out" >&2 | |
| return 1 | |
| fi | |
| out=$(printf '%s' "$out" | tr -d '"' | tr -d '[:space:]') | |
| case "$out" in | |
| sha256:*) printf '%s' "$out" ;; | |
| *) echo "::error::unexpected digest for $ref: '$out'" >&2; return 1 ;; | |
| esac | |
| } | |
| for pair in "latest:${version}" "latest-iperf:${version}-iperf"; do | |
| floating="${pair%%:*}" | |
| pinned="${pair#*:}" | |
| a=$(digest_of "${repo}:${floating}") || exit 1 | |
| b=$(digest_of "${repo}:${pinned}") || exit 1 | |
| if [ "$a" != "$b" ]; then | |
| echo "::error::${floating} resolves to $a but ${pinned} is $b; the floating tag did not move" | |
| exit 1 | |
| fi | |
| done | |
| # The step that COMPLETES the release: everything above - assets, images, | |
| # and all three attestations - has succeeded, so flip the draft release | |
| # public. Deliberately the FINAL step, so a failure anywhere earlier | |
| # strands only an invisible draft (which the stable guard treats as | |
| # resumable - a plain re-run finishes the job), never a public-but- | |
| # incomplete release. --latest is applied here and ONLY here, for STABLE | |
| # tags: GoReleaser skips its own undraft/make_latest handling when | |
| # release.draft is true, and GitHub refuses to mark drafts or prereleases | |
| # as latest. On an rc re-cut the release is already published and this | |
| # edit is a harmless no-op (gh resolves drafts by their pending tag name, | |
| # published releases by tag). Same fail-closed retry shape as the guards: | |
| # if the flip cannot be confirmed, fail loudly - the release is still a | |
| # draft and a re-run resumes it. | |
| - name: Publish the release (flip out of draft) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| tag="${GITHUB_REF_NAME}" | |
| set -- release edit "$tag" --draft=false | |
| case "$tag" in | |
| *-*) ;; # prerelease: GitHub cannot mark it latest | |
| *) set -- "$@" --latest ;; | |
| esac | |
| set -- "$@" --repo "${GITHUB_REPOSITORY}" | |
| for attempt in 1 2 3; do | |
| if gh "$@"; then | |
| echo "release '$tag' is published" | |
| exit 0 | |
| fi | |
| echo "attempt $attempt: could not flip release '$tag' out of draft" | |
| sleep 5 | |
| done | |
| echo "::error::release '$tag' is complete but still a DRAFT: assets and attestations are all in place, only the flip failed. Re-run this workflow once the API is healthy - the stable guard treats a draft as resumable." | |
| exit 1 |