Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions .github/workflows/build_and_publish_container.yml
Comment thread
TomHennen marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,36 @@ on:
value: ${{ jobs.gate.outputs.should-release }}

jobs:
# Refuse trigger patterns known to expose this workflow to supply-chain
# attack classes (pull_request_target, workflow_run via pull_request_target).
# The guard MUST stay first under `jobs:`; every other job lists `guard`
# in its `needs:` so a refused invocation skips the entire workflow —
# critical for the container build type, whose docker push happens
# mid-composite (no verify step could roll back a pushed image).
# Full trigger contract: docs/SPEC.md#trigger-model.
guard:
runs-on: ubuntu-latest
permissions: {} # reads env only — no workspace, no API, no secrets
steps:
# TODO: replace with $/actions/preflight_guard when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/preflight_guard@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14

gate:
needs: [guard]
runs-on: ubuntu-latest
permissions: {} # release_gate reads only env; no workspace, no API
outputs:
should-release: ${{ steps.gate.outputs.should-release }}
steps:
# No checkout: release_gate resolves via ${{ github.action_path }}.
# TODO: replace with $/actions/release_gate when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/release_gate@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
- uses: TomHennen/wrangle/actions/release_gate@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
id: gate
with:
events: ${{ inputs.release-events }}

build:
needs: [guard]
permissions:
contents: read
packages: write
Expand All @@ -94,7 +110,7 @@ jobs:
# TODO: replace with $/build/actions/container when GitHub ships $/ syntax (#136)
- name: "build and publish"
id: build
uses: TomHennen/wrangle/build/actions/container@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
uses: TomHennen/wrangle/build/actions/container@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
with:
path: ${{ inputs.path }}
imagename: ${{ inputs.imagename }}
Expand All @@ -105,7 +121,7 @@ jobs:
# Gated on release-events (default: non-pull-request) so adopters can
# tighten when provenance is produced without forking the workflow.
if: ${{ needs.gate.outputs.should-release == 'true' }}
needs: [gate, build]
needs: [guard, gate, build]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
Expand Down Expand Up @@ -138,7 +154,7 @@ jobs:
# Tests assert the lockstep so a single-side bump fails CI loudly.
verify:
if: ${{ needs.gate.outputs.should-release == 'true' && inputs.verify-image }}
needs: [gate, build, provenance]
needs: [guard, gate, build, provenance]
runs-on: ubuntu-latest
# No registry credentials needed: cosign verify-attestation against
# ghcr.io public images works anonymously, and the SLSA attestation
Expand Down
22 changes: 18 additions & 4 deletions .github/workflows/build_and_publish_npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,34 @@ on:
value: ${{ jobs.gate.outputs.should-release }}

jobs:
# Refuse trigger patterns known to expose this workflow to supply-chain
# attack classes (pull_request_target, workflow_run via pull_request_target).
# The guard MUST stay first under `jobs:`; every other job lists `guard`
# in its `needs:` so a refused invocation skips the entire workflow.
# Full trigger contract: docs/SPEC.md#trigger-model.
guard:
runs-on: ubuntu-latest
permissions: {} # reads env only — no workspace, no API, no secrets
steps:
# TODO: replace with $/actions/preflight_guard when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/preflight_guard@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14

gate:
needs: [guard]
runs-on: ubuntu-latest
permissions: {} # release_gate reads only env; no workspace, no API
outputs:
should-release: ${{ steps.gate.outputs.should-release }}
steps:
# No checkout: release_gate resolves via ${{ github.action_path }}.
# TODO: replace with $/actions/release_gate when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/release_gate@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
- uses: TomHennen/wrangle/actions/release_gate@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
id: gate
with:
events: ${{ inputs.release-events }}

build:
needs: [guard]
runs-on: ubuntu-latest
permissions:
contents: read # minimal — no OIDC, no publish
Expand All @@ -146,7 +160,7 @@ jobs:
ref: ${{ inputs.ref || '' }}

# TODO: replace with $/build/actions/npm when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/build/actions/npm@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
- uses: TomHennen/wrangle/build/actions/npm@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
id: build
with:
path: ${{ inputs.path }}
Expand Down Expand Up @@ -188,7 +202,7 @@ jobs:
# write is mandatory even when upload-assets is false.
provenance:
if: ${{ needs.gate.outputs.should-release == 'true' }}
needs: [gate, build]
needs: [guard, gate, build]
permissions:
actions: read # detect the GitHub Actions environment
id-token: write # OIDC for Sigstore signing
Expand All @@ -210,7 +224,7 @@ jobs:
# flows pass `verify-provenance: false` and wire their own.
verify:
if: ${{ needs.gate.outputs.should-release == 'true' && inputs.verify-provenance }}
needs: [gate, build, provenance]
needs: [guard, gate, build, provenance]
runs-on: ubuntu-latest
permissions: {} # default token; download-artifact reads same-run artifacts
steps:
Expand Down
22 changes: 18 additions & 4 deletions .github/workflows/build_and_publish_python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,24 +94,38 @@ on:
value: ${{ jobs.gate.outputs.should-release }}

jobs:
# Refuse trigger patterns known to expose this workflow to supply-chain
# attack classes (pull_request_target, workflow_run via pull_request_target).
# The guard MUST stay first under `jobs:`; every other job lists `guard`
# in its `needs:` so a refused invocation skips the entire workflow.
# Full trigger contract: docs/SPEC.md#trigger-model.
guard:
runs-on: ubuntu-latest
permissions: {} # reads env only — no workspace, no API, no secrets
steps:
# TODO: replace with $/actions/preflight_guard when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/preflight_guard@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14

# Decide whether release-time actions (provenance, publish) should run
# for this event. Cheap separate job so the result is available to
# both the provenance job below and the caller's publish job via
# outputs.should-release.
gate:
needs: [guard]
runs-on: ubuntu-latest
permissions: {} # release_gate reads only env; no workspace, no API
outputs:
should-release: ${{ steps.gate.outputs.should-release }}
steps:
# No checkout: release_gate resolves via ${{ github.action_path }}.
# TODO: replace with $/actions/release_gate when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/release_gate@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
- uses: TomHennen/wrangle/actions/release_gate@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
id: gate
with:
events: ${{ inputs.release-events }}

build:
needs: [guard]
runs-on: ubuntu-latest
permissions:
contents: read # minimal — no OIDC, no publish
Expand All @@ -128,7 +142,7 @@ jobs:
ref: ${{ inputs.ref || '' }}

# TODO: replace with $/build/actions/python when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/build/actions/python@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
- uses: TomHennen/wrangle/build/actions/python@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
id: build
with:
path: ${{ inputs.path }}
Expand Down Expand Up @@ -173,7 +187,7 @@ jobs:
# only uploads provenance as a workflow artifact (90-day retention).
provenance:
if: ${{ needs.gate.outputs.should-release == 'true' }}
needs: [gate, build]
needs: [guard, gate, build]
permissions:
actions: read # detect the GitHub Actions environment
id-token: write # OIDC for Sigstore signing
Expand Down Expand Up @@ -202,7 +216,7 @@ jobs:
# `verify-provenance: false` and wire their own.
verify:
if: ${{ needs.gate.outputs.should-release == 'true' && inputs.verify-provenance }}
needs: [gate, build, provenance]
needs: [guard, gate, build, provenance]
runs-on: ubuntu-latest
permissions: {} # default token; download-artifact reads same-run artifacts
steps:
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/build_shell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,20 @@ on:
default: ""

jobs:
# Refuse trigger patterns known to expose this workflow to supply-chain
# attack classes (pull_request_target, workflow_run via pull_request_target).
# The guard MUST stay first under `jobs:`; every other job lists `guard`
# in its `needs:` so a refused invocation skips the entire workflow.
# Full trigger contract: docs/SPEC.md#trigger-model.
guard:
runs-on: ubuntu-latest
permissions: {} # reads env only — no workspace, no API, no secrets
steps:
# TODO: replace with $/actions/preflight_guard when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/preflight_guard@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14

shell-build:
needs: [guard]
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -30,7 +43,7 @@ jobs:

- name: Run shell build
# TODO: replace with $/build/actions/shell when GitHub ships $/ syntax (#136)
uses: TomHennen/wrangle/build/actions/shell@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
uses: TomHennen/wrangle/build/actions/shell@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
with:
scan-path: ${{ inputs.scan-path }}
bats-path: ${{ inputs.bats-path }}
15 changes: 14 additions & 1 deletion .github/workflows/check_source_change.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,20 @@ on:
default: "osv zizmor scorecard:info"

jobs:
# Refuse trigger patterns known to expose this workflow to supply-chain
# attack classes (pull_request_target, workflow_run via pull_request_target).
# The guard MUST stay first under `jobs:`; every other job lists `guard`
# in its `needs:` so a refused invocation skips the entire workflow.
# Full trigger contract: docs/SPEC.md#trigger-model.
guard:
runs-on: ubuntu-latest
permissions: {} # reads env only — no workspace, no API, no secrets
steps:
# TODO: replace with $/actions/preflight_guard when GitHub ships $/ syntax (#136)
- uses: TomHennen/wrangle/actions/preflight_guard@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14

check-change:
needs: [guard]
permissions:
actions: read
contents: read
Expand All @@ -25,6 +38,6 @@ jobs:
# action-pattern tools (zizmor, scorecard) via uses: internally.
# TODO: replace with $/actions/scan when GitHub ships $/ syntax (#136)
- name: Source scan
uses: TomHennen/wrangle/actions/scan@dad131c0f4faf6348952ad21b037750cb568834a # claude/v0.2-pnpm-support 2026-05-14
uses: TomHennen/wrangle/actions/scan@8f40e053e1423fb59008501f83126d7ff25fcae4 # claude/202-refuse-pull-request-target 2026-05-14
with:
tools: ${{ inputs.tools }}
27 changes: 27 additions & 0 deletions actions/preflight_guard/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Wrangle Preflight Guard
description: >
Refuse trigger patterns that would expose wrangle's reusable workflows
to known supply-chain attack classes. Used at the head of every wrangle
reusable workflow so a refused invocation skips the entire run
(`needs: [guard]` propagation).

Currently refuses:
- pull_request_target invocations
- workflow_run invocations triggered by pull_request_target
These are the "pwn request" vector that compromised TanStack/router via
Mini Shai-Hulud in May 2026. No legitimate wrangle adoption uses these
triggers for build/publish workflows.

No inputs. Wrangle owns the refuse-list; adopters add `needs: [guard]`
to their downstream jobs and the rest is automatic.

See docs/SPEC.md#trigger-model for the full trigger contract.

runs:
using: composite
steps:
- shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
OUTER_EVENT: ${{ github.event.workflow_run.event }}
run: "${{ github.action_path }}/preflight_guard.sh"
33 changes: 33 additions & 0 deletions actions/preflight_guard/preflight_guard.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# preflight_guard.sh — refuse trigger patterns known to expose wrangle's
# reusable workflows to supply-chain attack classes. Fails the workflow
# fast; downstream jobs with `needs: [guard]` skip via standard
# `needs:` propagation.
#
# Required env (set by the composite action wrapper):
# EVENT_NAME — github.event_name
# OUTER_EVENT — github.event.workflow_run.event (empty unless event is
# workflow_run)
#
# Refusals are listed below. New refusal categories belong here, with a
# matching entry in docs/SPEC.md#trigger-model.

set -euo pipefail
set -f # env vars come from GitHub event context — disable globbing defensively

fail() {
printf '::error::wrangle refuses %s.\n' "$1" >&2
printf "::error::This trigger pattern combined with a checkout of PR head SHA is the 'pwn request' vector that compromised TanStack/router via Mini Shai-Hulud in May 2026.\n" >&2
printf '::error::Move build/publish to a push-triggered workflow (push to main or tags). See docs/SPEC.md#trigger-model.\n' >&2
exit 1
}

if [[ "$EVENT_NAME" == "pull_request_target" ]]; then
fail "pull_request_target invocations"
fi

if [[ "$EVENT_NAME" == "workflow_run" && "$OUTER_EVENT" == "pull_request_target" ]]; then
fail "workflow_run invocations triggered by pull_request_target"
fi

printf 'Event "%s" allowed.\n' "$EVENT_NAME"
Loading
Loading