feat(preflight): fail early when a secret or repository access is missing

A release run used to fail at the GoReleaser step when a secret was
empty, after the tests and the version job had finished.

A new preflight action checks the required names, the .Env references
in the GoReleaser config and push access to optional repositories. It
lists every problem in one run. The release and release-cgo workflows
run it in a first job, so a missing secret fails in seconds.

The release-cgo workflow requires GORELEASER_PRO. The release workflow
requires the secrets that the GoReleaser config references. Set the
preflight input to false to skip the check. Set preflight-repos to check
access to a Homebrew tap or helm-charts repository.
This commit is contained in:
2026-10-02 01:23:56 +01:00
parent 6b47cbd317
commit f9b22ef8d9
4 changed files with 174 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
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"
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 }}
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 [ "${#missing[@]}" -gt 0 ]; then
echo "Preflight failed: ${missing[*]}"
exit 1
fi
echo "Preflight passed"
+34
View File
@@ -65,6 +65,16 @@ on:
required: false required: false
type: string type: string
default: "" default: ""
preflight:
description: "Fail early when a required secret, env var or GoReleaser .Env reference is missing"
required: false
type: boolean
default: true
preflight-repos:
description: "Comma-separated owner/repo list the token must have push access to (e.g. a Homebrew tap or helm-charts repo)"
required: false
type: string
default: ""
outputs: outputs:
version: version:
description: "The calculated version (without v prefix)" description: "The calculated version (without v prefix)"
@@ -78,8 +88,31 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
preflight:
name: Preflight
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 5
steps:
- name: Checkout
if: inputs.preflight
uses: actions/checkout@v6
with:
lfs: false
- name: Check secrets and access
if: inputs.preflight
uses: lukaszraczylo/shared-actions/.github/actions/preflight@main
with:
require: GITHUB_TOKEN,GORELEASER_KEY
repo-access: ${{ inputs.preflight-repos }}
repo-token-env: GITHUB_TOKEN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_PRO }}
test: test:
name: Test name: Test
needs: preflight
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }} runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
@@ -105,6 +138,7 @@ jobs:
frontend: frontend:
name: Build Frontend name: Build Frontend
needs: preflight
if: inputs.node-enabled && inputs.node-build-script != '' if: inputs.node-enabled && inputs.node-build-script != ''
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }} runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 20 timeout-minutes: 20
+32
View File
@@ -38,6 +38,16 @@ on:
required: false required: false
type: string type: string
default: "" default: ""
preflight:
description: "Fail early when a required secret, env var or GoReleaser .Env reference is missing"
required: false
type: boolean
default: true
preflight-repos:
description: "Comma-separated owner/repo list the token must have push access to (e.g. a Homebrew tap or helm-charts repo)"
required: false
type: string
default: ""
outputs: outputs:
version: version:
description: "The calculated version (without v prefix)" description: "The calculated version (without v prefix)"
@@ -51,8 +61,30 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
preflight:
name: Preflight
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 5
steps:
- name: Checkout
if: inputs.preflight
uses: actions/checkout@v6
with:
lfs: false
- name: Check secrets and access
if: inputs.preflight
uses: lukaszraczylo/shared-actions/.github/actions/preflight@main
with:
require: GITHUB_TOKEN
repo-access: ${{ inputs.preflight-repos }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
test: test:
name: Test name: Test
needs: preflight
runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }} runs-on: ${{ fromJSON(startsWith(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest', '{') && inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest' || toJSON(inputs.runner || vars.SHARED_ACTIONS_RUNNER || 'ubuntu-latest')) }}
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
+20
View File
@@ -154,6 +154,26 @@ Run GoReleaser with mode support.
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
``` ```
### `actions/preflight`
Fail in seconds when a prerequisite is missing, instead of at the end of a long run. Reads names from the step `env`, so map secrets there.
```yaml
- uses: lukaszraczylo/shared-actions/.github/actions/preflight@main
with:
require: GITHUB_TOKEN,NPM_TOKEN # must be non-empty
repo-access: lukaszraczylo/helm-charts # token needs push access
repo-token-env: HOMEBREW_TAP_TOKEN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
```
It also scans `.goreleaser.y*ml` and requires every `{{ .Env.NAME }}` (comment lines ignored) to be set. All problems are listed in one run.
`go-release.yaml` and `go-release-cgo.yaml` run it in a first `preflight` job before tests. Set `preflight: false` to skip it, and `preflight-repos` to check access to a tap or helm-charts repo. An empty secret usually means the caller omitted `secrets: inherit`.
### `actions/rolling-release` ### `actions/rolling-release`
Create/update a rolling release tag. Create/update a rolling release tag.