diff --git a/.github/actions/preflight/action.yml b/.github/actions/preflight/action.yml new file mode 100644 index 0000000..2705aae --- /dev/null +++ b/.github/actions/preflight/action.yml @@ -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" diff --git a/.github/workflows/go-release-cgo.yaml b/.github/workflows/go-release-cgo.yaml index 2c2a258..dd068bb 100644 --- a/.github/workflows/go-release-cgo.yaml +++ b/.github/workflows/go-release-cgo.yaml @@ -65,6 +65,16 @@ on: required: false type: string 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: version: description: "The calculated version (without v prefix)" @@ -78,8 +88,31 @@ concurrency: cancel-in-progress: false 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: 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')) }} timeout-minutes: 30 steps: @@ -105,6 +138,7 @@ jobs: frontend: name: Build Frontend + needs: preflight 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')) }} timeout-minutes: 20 diff --git a/.github/workflows/go-release.yaml b/.github/workflows/go-release.yaml index 01ca081..eae8754 100644 --- a/.github/workflows/go-release.yaml +++ b/.github/workflows/go-release.yaml @@ -38,6 +38,16 @@ on: required: false type: string 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: version: description: "The calculated version (without v prefix)" @@ -51,8 +61,30 @@ concurrency: cancel-in-progress: false 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: 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')) }} timeout-minutes: 30 steps: diff --git a/README.md b/README.md index 904cd6a..87f5eb3 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,26 @@ Run GoReleaser with mode support. 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` Create/update a rolling release tag.