diff --git a/catalog/snippets/workflows/README.md b/catalog/snippets/workflows/README.md index 49265ae9..a2e22f1c 100644 --- a/catalog/snippets/workflows/README.md +++ b/catalog/snippets/workflows/README.md @@ -6,6 +6,7 @@ The reusable build/publish workflow tasks a code-shipping repo runs. They are ** | --- | --- | --- | | `build-release-task.yml` | Multi-target release orchestrator: get-version, validate-release, github-release plus per-target build jobs | D3, D4, D5, D6 | | `get-version-task.yml` | NBGV version/tag computation (reusable) | D3 | +| `publish-plan-task.yml` | Single-source release-gate decision (publish? stable?) reused by every publish-release job | D4 | | `build-executable-task.yml` | Console/executable per-runtime publish, aggregate to one release asset | D5, D6; section 6 Console walkthrough | | `build-nugetlibrary-task.yml` | Build + `dotnet nuget push` (OIDC), upload release asset | D3.4, D4.4, D6; section 6 NuGet walkthrough | | `build-pypilibrary-task.yml` | Build PyPI package; publish split to an OIDC job | D3.4, D4, D7.2; section 6 PyPI walkthrough | diff --git a/catalog/snippets/workflows/publish-plan-task.yml b/catalog/snippets/workflows/publish-plan-task.yml new file mode 100644 index 00000000..ab7dbbb9 --- /dev/null +++ b/catalog/snippets/workflows/publish-plan-task.yml @@ -0,0 +1,84 @@ +name: Publish plan task + +# Single source of truth for the release-gate decision, reused by every publish-release.yml job so the policy +# lives here, not scattered across job `if:` conditions. A human PR merge never auto-publishes; a release is a +# deliberate dispatch, a bot (Dependabot/codegen) code-merge to main, or the Docker weekly schedule. +# +# Outputs: +# publish - 'true' when this run should publish: a bot-authored push (the codegen App merges every bot PR, so +# its identity - or dependabot[bot] - is the gate), a schedule, or a workflow_dispatch of main/develop. +# A human push (a merge/promotion to main) or a dispatch from any other branch is 'false'. +# stable - 'true' when the target branch is main (stable channel); main-only jobs gate on publish && stable. +# Both outputs are the strings 'true'/'false' - gate with == 'true'; a bare `if: ${{ needs.plan.outputs.publish }}` +# is always truthy (a non-empty string is truthy in an Actions expression). +# +# Shared across repo types: a library/package repo triggers only push + dispatch and uses `publish`; a +# Docker/wrapper repo also triggers the weekly schedule and gates main-only jobs on `stable`. A case a given +# caller never triggers (e.g. schedule for a library) is simply inert for it - expected of a single-source task. + +on: + workflow_call: + inputs: + event_name: + description: The triggering event (github.event_name). + required: true + type: string + actor: + description: The actor that triggered the run (github.actor). + required: true + type: string + ref_name: + description: The short ref name (github.ref_name). + required: true + type: string + outputs: + publish: + description: "'true' when this run should publish." + value: ${{ jobs.plan.outputs.publish }} + stable: + description: "'true' when the target branch is main (stable channel)." + value: ${{ jobs.plan.outputs.stable }} + +jobs: + + plan: + name: Plan release job + runs-on: ubuntu-latest + outputs: + publish: ${{ steps.decide.outputs.publish }} + stable: ${{ steps.decide.outputs.stable }} + + steps: + + - name: Decide release plan step + id: decide + env: + EVENT: ${{ inputs.event_name }} + ACTOR: ${{ inputs.actor }} + REF: ${{ inputs.ref_name }} + run: | + set -euo pipefail + publish=false + case "$EVENT" in + workflow_dispatch) + # A human release: only the long-lived branches publish (a stray feature-branch dispatch is a no-op). + [[ "$REF" == "main" || "$REF" == "develop" ]] && publish=true + ;; + schedule) + # Docker weekly refresh (main-only by schedule config). + publish=true + ;; + push) + # A human merge never auto-publishes; only a bot merge to main does. The codegen App merges every + # Dependabot/codegen PR, so github.actor is its identity (dependabot[bot] allowed defensively). The + # ref==main guard keeps the task self-contained even if a caller's push trigger is not main-only. + if [[ "$REF" == "main" ]] && { [[ "$ACTOR" == "ptr727-codegen[bot]" ]] || [[ "$ACTOR" == "dependabot[bot]" ]]; }; then + publish=true + fi + ;; + esac + stable=false + [[ "$REF" == "main" ]] && stable=true + echo "publish=$publish" >> "$GITHUB_OUTPUT" + echo "stable=$stable" >> "$GITHUB_OUTPUT" + echo "Release plan: event=$EVENT actor=$ACTOR ref=$REF -> publish=$publish stable=$stable"