Skip to content

feat(empty-pr-guard): composite action + notify alert-card inputs - #20

Merged
arnavchachra merged 4 commits into
mainfrom
feat/empty-pr-guard-composite
Jul 17, 2026
Merged

feat(empty-pr-guard): composite action + notify alert-card inputs#20
arnavchachra merged 4 commits into
mainfrom
feat/empty-pr-guard-composite

Conversation

@tarpanpathak

@tarpanpathak tarpanpathak commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Alternative shape for #19. Same behaviour, packaged as a composite action instead of a reusable workflow, so callers write less YAML.

What

  1. actions/enforce/action.yml (new). Merge-time gate as a composite action. On a push, resolves the PR behind github.sha and, if its body is empty or whitespace-only, reverts the merge on the pushed branch, edits the PR to explain, and returns block=true. Behaviour identical to the reusable-workflow prototype in feat(empty-pr-guard): reusable empty-PR-description gate + notify alert cards #19: PR-fetch fails safe, all three merge styles (merge / squash / rebase) fully undone, PR body edited with an auto-revert notice.

  2. actions/notify/action.yml. Three optional inputs (header, color, status) that swap the deploy card into a general alert. Defaults reproduce the existing card byte-for-byte, so existing deploy callers are unaffected. Same change as feat(empty-pr-guard): reusable empty-PR-description gate + notify alert cards #19.

  3. README.md. Contents table row for actions/enforce, an "enforce inputs & outputs" section with a worked example, and a "Why a composite action rather than a reusable workflow?" subsection recording the design rationale.

Why composite instead of reusable workflow

A reusable-workflow guard forces callers to declare two jobs: one to run the guard, and a second *-notify job that carries environment: so the env-scoped Slack webhook resolves. Cross-job output threading (needs.guard.outputs.header) glues them together.

A composite runs inside the caller's job, so:

  • The env-scoped webhook is already in scope, no separate alert job needed.
  • Alert content flows via step outputs, not needs.<x>.outputs.*.
  • The caller declares one job with two steps.

Same internal script, same behaviour, less caller YAML and one fewer node in the pipeline graph.

Compatibility

  • notify: additive. header / color / status default to "" and existing deploy callers produce byte-identical output.
  • enforce: brand new, no existing callers.
  • Ships by moving @v3 forward. Additive minor bump, no new major.

Caller-side comparison

Reusable-workflow shape (#19):

guard:
  uses: nurdsoft/ci-workflows/.github/workflows/empty-pr-guard.yml@v3
  permissions: { contents: write, pull-requests: write }
  with: { label: Service }

guard-notify:
  needs: [guard]
  if: ${{ always() && needs.guard.outputs.outcome != '' }}
  runs-on: ubuntu-latest
  environment: dev
  permissions: { contents: read, pull-requests: read }
  steps:
    - uses: nurdsoft/ci-workflows/actions/notify@v3
      with:
        result: failure
        webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
        label: Service
        header: ${{ needs.guard.outputs.header }}
        color:  ${{ needs.guard.outputs.color }}
        status: ${{ needs.guard.outputs.reason }}

Composite-action shape (this PR):

guard:
  runs-on: ubuntu-latest
  environment: dev
  permissions: { contents: write, pull-requests: write }
  outputs:
    block: ${{ steps.g.outputs.block }}
  steps:
    - id: g
      uses: nurdsoft/ci-workflows/actions/enforce@v3
      with: { label: Service }
    - if: ${{ steps.g.outputs.outcome != '' }}
      uses: nurdsoft/ci-workflows/actions/notify@v3
      with:
        result: failure
        webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
        label: Service
        header: ${{ steps.g.outputs.header }}
        color:  ${{ steps.g.outputs.color }}
        status: ${{ steps.g.outputs.reason }}

Two jobs become one. Cross-job output threading becomes step outputs. Everything downstream (needs.guard.outputs.block) works identically.

Test plan

  • action.yml (both) validate as YAML.
  • Notify defaults produce byte-identical output to the current deploy card.
  • Wired into a real caller pinned at @feat/empty-pr-guard-composite and validated end-to-end on dev:
    • Control case (PR with a non-empty description, merge commit): guard step succeeded, notify step correctly skipped, downstream release/build/deploy/announce all ran green.
    • Empty-body case (PR merged with empty body, merge commit): guard reverted the merge via git revert -m 1, revert commit pushed to dev, notify step fired, downstream release/build/deploy/announce all skipped via needs.guard.outputs.block != 'true', PR body edited with the auto-revert notice.

For the reviewer (@arnavchachra)

Once you approve and merge this to main, please move the @v3 tag to the merge commit so callers pinned to @v3 pick this up. Minor bump is appropriate (matches the additive nature of the notify change).

Notes

Ports the empty-PR-description guard as a composite action (actions/empty-pr-guard)
alongside the notify header/color/status inputs it needs. Composite-action shape
lets the caller run the guard and its Slack alert in the same job, so the
environment-scoped webhook resolves inline and the caller drops the separate
alert job + output-plumbing that a reusable-workflow shape requires.

The internal script and behaviour are the ones proven on the reusable-workflow
prototype: PR-fetch fails safe, all three merge styles reverted, PR body edited
to explain the auto-revert. Notify gains three optional inputs (header, color,
status) that swap the deploy card into a general alert; defaults reproduce the
existing card byte-for-byte, so existing deploy callers are unaffected.

Ships by moving the @V3 tag forward — additive, no new major.
All existing shared actions use a single-word directory name (build, deploy,
verify, notify, plan, apply, setup, auth). Rename actions/empty-pr-guard to
actions/guard to match. The action's display name (Empty PR-description guard)
stays as-is so workflow logs remain self-describing.
'guard' was ambiguous (could mean anything protecting anything). 'enforce'
describes what the action does (enforce a merge-time policy) and stays
single-word to match the existing convention (build, deploy, verify, notify,
plan, apply, setup, auth). Extensible: future policy rules (missing linked
issue, unsigned commit, etc.) can be modes of this same action rather than
proliferating similar-named actions.

Display name updated to 'Enforce PR description' so workflow logs stay
self-describing while the API surface (actions/enforce) matches convention.
@arnavchachra
arnavchachra merged commit 9720549 into main Jul 17, 2026
1 check passed
@arnavchachra
arnavchachra deleted the feat/empty-pr-guard-composite branch July 17, 2026 08:22
gamechanger1s added a commit that referenced this pull request Jul 17, 2026
Fold in #20 (actions/enforce + notify alert-card inputs) and address
tarpanpathak review feedback:

- Alert-mode branch in actions/notify: when header / color / status is
  set, swap the redesigned deploy card for the pre-v4 attachments-wrapper
  alert card (byte-identical). actions/enforce alert path survives v4.
  Skips run/jobs fetch on alert path (not needed there).
- Emit ::warning:: when /actions/runs or /actions/runs/{id}/jobs come
  back empty (loud fail if caller forgot actions: read; DURATION and
  FAILED AT otherwise drop silently).
- Maintenance-risk note above padlabel: Lato-empirical, drift-silent,
  fallback recipe recorded inline.
- README: enforce section restored, notify table gains header / color /
  status rows, label example changed from "Commerce Backend" to
  "Backend", @V3 example pins bumped to @v4 in preparation for the
  v4.0.0 cut.

Rollback anchor: 772fc1a (last
known-good HEAD before this merge; both cards validated by user).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants