Files
shared-actions/.github/actions/preflight/action.yml
T
lukaszraczylo 9533a6518f feat(preflight): probe release and ghcr push access
A caller can grant narrower permissions than the release job needs. The
release then fails at the end, when it creates the tag or pushes an
image.

The preflight job now declares the same contents and packages
permissions as the release job and probes with the real token. A git
push dry run checks that the token can create the tag and release. A
ghcr.io token request and a blob upload session check that the token can
push the image. The upload session creates no package.

The release-cgo workflow probes ghcr only when the GoReleaser config
builds ghcr.io images. The release workflow probes it when
docker-enabled is true and the registry is ghcr.io. A probe that cannot
reach a verdict warns and does not fail.
2026-10-02 01:27:01 +01:00

164 lines
7.3 KiB
YAML

name: "Preflight"
description: "Fail early when a required secret, env var or repository access is missing"
inputs:
require:
description: "Comma-separated names that must be set and non-empty in the step env"
required: false
default: ""
goreleaser-scan:
description: "Also require every .Env.NAME referenced by the GoReleaser config"
required: false
default: "true"
repo-access:
description: "Comma-separated owner/repo list the token must have push access to"
required: false
default: ""
repo-token-env:
description: "Name of the env var that holds the token for repo-access"
required: false
default: "HOMEBREW_TAP_TOKEN"
release-write:
description: "Probe that GITHUB_TOKEN can push to this repository (needed to create tags and releases)"
required: false
default: "false"
ghcr-push:
description: "Probe push access to ghcr.io: true, false, or auto (only when the GoReleaser config builds ghcr.io images)"
required: false
default: "false"
ghcr-image:
description: "Image path (owner/name) for the ghcr probe. Default: first ghcr.io path in the GoReleaser config, else the repository"
required: false
default: ""
ghcr-token-env:
description: "Name of the env var that holds the ghcr token"
required: false
default: "GITHUB_TOKEN"
runs:
using: "composite"
steps:
- name: Check prerequisites
shell: bash
env:
PF_REQUIRE: ${{ inputs.require }}
PF_SCAN: ${{ inputs.goreleaser-scan }}
PF_REPOS: ${{ inputs.repo-access }}
PF_TOKEN_ENV: ${{ inputs.repo-token-env }}
PF_RELEASE_WRITE: ${{ inputs.release-write }}
PF_GHCR: ${{ inputs.ghcr-push }}
PF_GHCR_IMAGE: ${{ inputs.ghcr-image }}
PF_GHCR_TOKEN_ENV: ${{ inputs.ghcr-token-env }}
run: |
missing=()
add_missing() { # name reason
case " ${missing[*]-} " in *" $1 "*) return ;; esac
missing+=("$1")
echo "::error title=Missing $1::$2"
}
is_set() { [ -n "${!1:-}" ]; }
IFS=',' read -r -a required <<< "$PF_REQUIRE"
for name in "${required[@]-}"; do
name="${name// /}"
[ -z "$name" ] && continue
is_set "$name" || add_missing "$name" "Required by this workflow but empty. Set the repository or organisation secret $name and make sure the caller passes 'secrets: inherit'."
done
if [ "$PF_SCAN" = "true" ]; then
for cfg in .goreleaser.yaml .goreleaser.yml goreleaser.yaml goreleaser.yml; do
[ -f "$cfg" ] || continue
echo "Scanning $cfg for .Env references"
while IFS= read -r ref; do
name="${ref#.Env.}"
is_set "$name" || add_missing "$name" "$cfg references .Env.$name but it is not available to the GoReleaser step. GoReleaser would fail at the end of the run."
done < <(grep -vE '^[[:space:]]*#' "$cfg" | grep -oE '\.Env\.[A-Za-z_][A-Za-z0-9_]*' | sort -u)
break
done
fi
if [ -n "$PF_REPOS" ]; then
token="${!PF_TOKEN_ENV:-}"
if [ -z "$token" ]; then
add_missing "$PF_TOKEN_ENV" "repo-access needs this token but it is empty."
else
IFS=',' read -r -a repos <<< "$PF_REPOS"
for repo in "${repos[@]}"; do
repo="${repo// /}"
[ -z "$repo" ] && continue
body="$(mktemp)"
code="$(curl -sS -o "$body" -w '%{http_code}' \
-H "Authorization: Bearer $token" \
-H "Accept: application/vnd.github+json" \
"${GITHUB_API_URL:-https://api.github.com}/repos/$repo")" || code="000"
if [ "$code" != "200" ]; then
add_missing "access:$repo" "$PF_TOKEN_ENV cannot read $repo (HTTP $code). Check the token scope, expiry and repository access."
elif ! grep -qE '"push": *true' "$body"; then
add_missing "access:$repo" "$PF_TOKEN_ENV can read $repo but has no push access."
fi
rm -f "$body"
done
fi
fi
if [ "$PF_RELEASE_WRITE" = "true" ]; then
out="$(git push --dry-run origin HEAD:refs/heads/preflight-probe 2>&1)" && rc=0 || rc=$?
if [ "$rc" -ne 0 ]; then
if printf '%s' "$out" | grep -qiE 'denied|403|401|not permitted|could not read Username'; then
add_missing "contents:write" "The token cannot push to this repository, so it cannot create the tag or release. Grant 'contents: write' in the caller workflow permissions and check the repository Actions settings. Git said: $(printf '%s' "$out" | tail -n 1)"
else
echo "::warning title=Push probe inconclusive::$(printf '%s' "$out" | tail -n 1)"
fi
fi
fi
ghcr_wanted="$PF_GHCR"
cfg_found=""
for cfg in .goreleaser.yaml .goreleaser.yml goreleaser.yaml goreleaser.yml; do
[ -f "$cfg" ] && { cfg_found="$cfg"; break; }
done
if [ "$ghcr_wanted" = "auto" ]; then
ghcr_wanted=false
if [ -n "$cfg_found" ] && grep -qE '^(dockers|dockers_v2|docker_manifests):' "$cfg_found" && grep -q 'ghcr\.io' "$cfg_found"; then
ghcr_wanted=true
fi
fi
if [ "$ghcr_wanted" = "true" ]; then
gtoken="${!PF_GHCR_TOKEN_ENV:-}"
image="$PF_GHCR_IMAGE"
if [ -z "$image" ] && [ -n "$cfg_found" ]; then
image="$(grep -oE 'ghcr\.io/[A-Za-z0-9._/-]+' "$cfg_found" | head -n 1 | sed 's|^ghcr\.io/||')"
fi
image="${image:-$GITHUB_REPOSITORY}"
image="$(printf '%s' "$image" | tr '[:upper:]' '[:lower:]')"
if [ -z "$gtoken" ]; then
add_missing "$PF_GHCR_TOKEN_ENV" "ghcr push needs this token but it is empty."
else
tbody="$(mktemp)"
code="$(curl -sS -o "$tbody" -w '%{http_code}' -u "${GITHUB_ACTOR:-token}:$gtoken" \
"https://ghcr.io/token?service=ghcr.io&scope=repository:$image:push,pull")" || code="000"
btoken="$(sed -n 's/.*"token" *: *"\([^"]*\)".*/\1/p' "$tbody" | head -n 1)"
rm -f "$tbody"
if [ "$code" = "200" ] && [ -n "$btoken" ]; then
ucode="$(curl -sS -o /dev/null -w '%{http_code}' -X POST -H "Authorization: Bearer $btoken" \
"https://ghcr.io/v2/$image/blobs/uploads/")" || ucode="000"
case "$ucode" in
202) ;;
401|403) add_missing "packages:write" "The token cannot push ghcr.io/$image (HTTP $ucode). Grant 'packages: write' in the caller workflow permissions, and for an existing package allow this repository under Package settings, Manage Actions access." ;;
*) echo "::warning title=ghcr probe inconclusive::upload session for $image returned HTTP $ucode" ;;
esac
elif [ "$code" = "401" ] || [ "$code" = "403" ]; then
add_missing "packages:write" "ghcr.io refused the token for $image (HTTP $code). Grant 'packages: write' in the caller workflow permissions."
else
echo "::warning title=ghcr probe inconclusive::token request for $image returned HTTP $code"
fi
fi
fi
if [ "${#missing[@]}" -gt 0 ]; then
echo "Preflight failed: ${missing[*]}"
exit 1
fi
echo "Preflight passed"