Skip to content
Merged
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
22 changes: 16 additions & 6 deletions .github/agent-guides/catalogue.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This repository also hosts the suite's shared CI/CD building blocks: composite a
[`.github/workflows/`](../../.github/workflows/). They were imported from `devantler-tech/actions` as one
snapshot (#235). The one-time importer is retired and preserved in Git history; the current
catalogue and its documentation are authored here, not generated from that historical source.
That repository is frozen and is archived once nothing references it (#164). Consumers reference
That repository is frozen pending verified migration and retirement (#240). Consumers reference
the catalogue here as:

```yaml
Expand All @@ -35,7 +35,7 @@ inside `test-create-release-config` in required CI. Never treat its diagnosis as
or write authorization; #587 owns publication, legacy compatibility and protected consumer proof.

The same Go module holds a separate `cmd/publish-release-recovery` command. Only the default-off
reusable `publish-release-recovery.yaml` calls it, after a confirmed manual dispatch, a read-only
repository-owned manual `publish-release-recovery.yaml` calls it, after a confirmed dispatch, a read-only
admission and only then a writer token; run `bash .github/tests/test-publish-release-recovery.sh`
after changing it. It re-runs the inspector's admission with its writer credential, so the inspection command
stays read-only. It refuses to publish unless that credential can push, two complete release
Expand All @@ -48,6 +48,13 @@ that claims nothing it did not check. It never retries a write. A server
error, a lost connection or a failed readback is reported as an unknown outcome, naming any release
it created, and a later run reconciles it through fresh reads.

No production consumer adopted this recovery workflow. It is not a public reusable interface:
other products own their recovery workflow until real repeated use justifies extraction.
`release-recovery-offline-fixture.yaml` is an internal generated projection containing only the
complete credential-free offline job, with no publisher job or secret interface. Regenerate it
with `bash .github/scripts/generate-release-recovery-fixture.sh`; the independent production
parity, generator equality, native enabled/disabled controls and omission mutations stay required.

The credential-free release fixture also exercises stranded stable tags with absent or matching
default-channel notes, for both lightweight and annotated tags. It verifies no release at the tagged
commit, the next patch after a subsequent fix, and unchanged consumer files and local/remote refs.
Expand All @@ -63,8 +70,8 @@ actions/<action-name>/ # One directory per composite action
.scripts/ # Scripts shared by composite actions, reached as ${GITHUB_ACTION_PATH}/../../.scripts

.github/
├── workflows/ # ALL workflows (GitHub requires it). A workflow with `workflow_call` is a
│ # catalogue product; repository-owned workflows are listed below.
├── workflows/ # ALL workflows (GitHub requires it). Public `workflow_call` products
│ # and explicitly internal generated fixtures are indexed separately.
├── actions/ # internal composite actions the reusable workflows check out and run
├── fixtures/ # fixtures for the action `test-<action>` jobs
├── scripts/ # helpers the reusable workflows run
Expand Down Expand Up @@ -98,8 +105,10 @@ One component may reference another **in this repository**. GitHub resolves thes
- **CI job → workspace action:** existing `uses: ./actions/<action>` references require
`actions/checkout`. They resolve from workspace bytes, not the defining file; the
broader native-reference migration and its cross-repository proof are tracked in #271.
- **Reusable workflow → co-located reusable workflow:** normally
`uses: ./.github/workflows/<x>.yaml` (same repository, same commit). An organization-required
- **Reusable workflow → co-located reusable workflow:** new calls use native
`uses: $/.github/workflows/<x>.yaml` (the defining file's repository and commit).
Existing classic `./.github/workflows/` calls retain their source-boundary controls during #271.
An organization-required
workflow is injected into a consumer repository, where that path resolves against the consumer
instead; it must call a sibling workflow through a full remote reference pinned to an immutable
commit whose workflow content has been reviewed, and every caller job is covered by a regression
Expand Down Expand Up @@ -197,6 +206,7 @@ cutover; the generic budget bound is not permission to omit tests or declare tha
| `measure-go-disk.yaml` | This repository |
| `organization-settings-audit.yaml` | This repository |
| `pr-metadata-guards.yaml` | This repository |
| `publish-release-recovery.yaml` | This repository; default-off confirmed manual recovery, not a reusable consumer interface |
| `refresh-gh-digests.yaml` | This repository |
| `release.yaml` | This repository |
| `repository-admin-team-audit.yaml` | This repository |
Expand Down
9 changes: 8 additions & 1 deletion .github/agent-guides/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ Actions and reusable workflows are exercised as jobs inside [`ci.yaml`](../../.g

`ci-required-checks` is the sole exception to the harden-runner-first rule: adding any action would weaken its workspace-independent trust boundary. Every other step-bearing job must start with SHA-pinned `step-security/harden-runner` in audit mode, and `lint-ci-coverage-parity` enforces both sides of that contract.

Reusable-workflow coverage reads parsed job-level `uses` values and requires an exact classic
`./.github/workflows/` or native `$/.github/workflows/` match. New same-repository calls use native
syntax. A comment, step-level reference, display name or filename prefix is not job execution.
The real inline guard's behavioral controls cover both forms and those non-call refusals.

**TODO coverage:** both TODO smoke callers use the complete generated
`scan-for-todo-comments-readonly.yaml` projection with contents-read permissions and no
App secret. Regenerate with `bash .github/scripts/generate-todo-readonly.sh`; never edit
Expand Down Expand Up @@ -240,7 +245,9 @@ A new job/step/behaviour must not go live for every consumer the moment it merge

**Gotchas:**

- `workflow_dispatch` passes booleans as **strings** while `workflow_call` passes real **booleans** — a workflow triggered by both must normalize before comparing: `if: ${{ inputs.<enable-x> == true || inputs.<enable-x> == 'true' }}`.
- The `inputs` context preserves declared booleans for both `workflow_dispatch` and `workflow_call`.
Only `github.event.inputs` converts manual inputs to strings. Use `inputs.<enable-x>` for typed
gates; see [GitHub's input-context documentation](https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow#defining-inputs-for-manually-triggered-workflows).
- You **cannot** wrap `if:` around an individual `with:` input — gate the whole **step** (or **job**), or select the value with a ternary in the `with:` value (`${{ inputs.<enable-x> && 'a' || 'b' }}`).
- **Test both states.** Per the feature-flag-first "test both states" rule, cover the flag **off** (default) and **on** with `[Test]` jobs (see *Test jobs* above) — a flag whose off-path is untested can regress silently.

Expand Down
20 changes: 20 additions & 0 deletions .github/scripts/generate-release-recovery-fixture.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Project only the complete credential-free offline job; never export the publisher.
set -euo pipefail
[[ $# -le 2 ]] || { echo 'Usage: generate-release-recovery-fixture.sh [source [destination]]' >&2; exit 2; }
root="$(cd "$(dirname "$0")/../.." && pwd)"
source="${1:-$root/.github/workflows/publish-release-recovery.yaml}"
destination="${2:-$root/.github/workflows/release-recovery-offline-fixture.yaml}"
work="$(mktemp)"
trap 'rm -f "$work"' EXIT
yq '
.name = "Internal release-recovery offline fixture" |
.on = {"workflow_call": {"inputs": (.on.workflow_dispatch.inputs |
with_entries(select(.key == "enable-recovery" or .key == "offline-test")))}} |
.jobs = {"offline-test": .jobs.offline-test}
' "$source" > "$work"
{
printf '# Generated by .github/scripts/generate-release-recovery-fixture.sh; do not edit.\n'
printf '# Internal credential-free native validation only, not a supported consumer interface.\n'
cat "$work"
} > "$destination"
49 changes: 34 additions & 15 deletions .github/tests/test-publish-release-recovery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ fail() { printf 'release recovery: %s\n' "$1" >&2; exit 1; }
[[ -f "$workflow" ]] || fail 'recovery workflow is missing'
yq -o=json '.' "$workflow" > "$work/workflow.json"
yq -o=json '.' "$ci" > "$work/ci.json"
jq -n --slurpfile w "$work/workflow.json" --slurpfile c "$work/ci.json" '{w:$w[0],c:$c[0]}' > "$work/bundle.json"
fixture="$root/.github/workflows/release-recovery-offline-fixture.yaml"
[[ -f "$fixture" ]] || fail 'internal recovery fixture is missing'
yq -o=json '.' "$fixture" > "$work/fixture.json"
jq -n --slurpfile w "$work/workflow.json" --slurpfile c "$work/ci.json" --slurpfile f "$work/fixture.json" '{w:$w[0],c:$c[0],f:$f[0]}' > "$work/bundle.json"
cat > "$work/guard.jq" <<'JQ'
.w as $w | .c as $c | $w.jobs.recover as $j | $j.steps as $s | $w.jobs["offline-test"] as $o |
def ordinary: .if == null and .["continue-on-error"] == null;
def writer: .if == "steps.read-admission.outputs.missing == 'true'" and .["continue-on-error"] == null;
def source: .env == {CATALOGUE_REPOSITORY:"${{ job.workflow_repository }}",CATALOGUE_SHA:"${{ job.workflow_sha }}"} and
def source: .env == {CATALOGUE_REPOSITORY:"${{ github.repository }}",CATALOGUE_SHA:"${{ github.workflow_sha }}"} and
(.run | contains("[[ \"$CATALOGUE_REPOSITORY\" == devantler-tech/.github && \"$CATALOGUE_SHA\" =~ ^[0-9a-f]{40}$ ]]")) and
(.run | contains("env -i PATH=\"$PATH\" curl --disable")) and
(.run | contains("https://codeload.github.com/$CATALOGUE_REPOSITORY/tar.gz/$CATALOGUE_SHA")) and
Expand All @@ -26,12 +29,12 @@ cat > "$work/guard.jq" <<'JQ'
(.run | contains("build -trimpath -o \"$RUNNER_TEMP/publish-release-recovery\" ./cmd/publish-release-recovery"));
def incident: {REPOSITORY:"${{ github.repository }}",TAG:"${{ inputs.tag }}",SOURCE_SHA:"${{ inputs.source-sha }}",RUN_ID:"${{ inputs.run-id }}",RUN_ATTEMPT:"${{ inputs.run-attempt }}",PRODUCER_REF:"${{ inputs.producer-ref }}"};
def token($contents): {"client-id":"${{ vars.APP_CLIENT_ID }}","private-key":"${{ secrets.APP_PRIVATE_KEY }}",owner:"devantler-tech",repositories:"${{ steps.admit.outputs.name }}","permission-contents":$contents,"permission-actions":"read"};
($w.on | keys) == ["workflow_call"] and $w.permissions == {} and
$w.on.workflow_call.inputs["enable-recovery"] == {description:$w.on.workflow_call.inputs["enable-recovery"].description,type:"boolean",default:false} and
$w.on.workflow_call.inputs["confirm-publication"].type == "boolean" and $w.on.workflow_call.inputs["confirm-publication"].default == false and
$w.on.workflow_call.inputs["offline-test"].default == false and
($w.on.workflow_call.inputs | has("repository") | not) and
($w.on.workflow_call.secrets | keys) == ["APP_PRIVATE_KEY"] and
($w.on | keys) == ["workflow_dispatch"] and $w.permissions == {} and
$w.on.workflow_dispatch.inputs["enable-recovery"] == {description:$w.on.workflow_dispatch.inputs["enable-recovery"].description,type:"boolean",default:false} and
$w.on.workflow_dispatch.inputs["confirm-publication"].type == "boolean" and $w.on.workflow_dispatch.inputs["confirm-publication"].default == false and
$w.on.workflow_dispatch.inputs["offline-test"].default == false and
($w.on.workflow_dispatch.inputs | has("repository") | not) and
$w.on.workflow_dispatch.secrets == null and
($w.jobs | keys) == ["offline-test","recover"] and
$j.if == "inputs.enable-recovery && !inputs.offline-test" and $j.permissions == {} and $j.needs == null and
$j.concurrency == {group:"create-release-${{ github.repository }}-${{ github.ref }}","cancel-in-progress":false,queue:"max"} and
Expand All @@ -55,22 +58,30 @@ cat > "$work/guard.jq" <<'JQ'
($o.steps[1] | source) and ($o.steps[3] | build) and
([$o.steps[] | tostring | select(test("secrets\\.|outputs\\.token|github\\.token"))] | length == 0) and ([$o.steps[] | select(.env.GH_TOKEN != null)] | length == 0) and
($o.steps[4].run | contains("unset GH_TOKEN GITHUB_TOKEN")) and
($c.jobs["test-publish-release-recovery"] | .uses == "./.github/workflows/publish-release-recovery.yaml" and .permissions == {} and .secrets == null and
.f == {name:"Internal release-recovery offline fixture",permissions:{},
on:{workflow_call:{inputs:($w.on.workflow_dispatch.inputs|with_entries(select(.key=="enable-recovery" or .key=="offline-test")))}},
jobs:{"offline-test":$o}} and
($c.jobs["test-publish-release-recovery"] | .uses == "$/.github/workflows/release-recovery-offline-fixture.yaml" and .permissions == {} and .secrets == null and
.with["enable-recovery"] == true and .with["offline-test"] == true and .with["confirm-publication"] == null) and
($c.jobs["test-publish-release-recovery-disabled"] | .uses == "./.github/workflows/publish-release-recovery.yaml" and .permissions == {} and .secrets == null and
($c.jobs["test-publish-release-recovery-disabled"] | .uses == "$/.github/workflows/release-recovery-offline-fixture.yaml" and .permissions == {} and .secrets == null and
.with["enable-recovery"] == null and .with["offline-test"] == null) and
(["test-publish-release-recovery","test-publish-release-recovery-disabled"] | all(. as $job |
($c.jobs["ci-required-checks"].needs | index($job) != null) and
($c.jobs["ci-required-checks"].steps | any((.env.JOB_RESULTS // "") | contains("needs." + $job + ".result"))))) and
($c.jobs["test-create-release-config"].steps | any(.run == "bash .github/tests/test-publish-release-recovery.sh" and ordinary))
JQ
jq -e -f "$work/guard.jq" "$work/bundle.json" >/dev/null || fail 'workflow authority, provenance or required coverage is unsafe'
# Exercise the generator too; the independent parsed equality above is the oracle.
bash "$root/.github/scripts/generate-release-recovery-fixture.sh" "$workflow" "$work/regenerated.yaml"
yq -o=json '.' "$work/regenerated.yaml" | jq -S . > "$work/regenerated.json"
jq -S . "$work/fixture.json" > "$work/checked.json"
cmp -s "$work/regenerated.json" "$work/checked.json" || fail 'internal recovery fixture needs regeneration'
# shellcheck disable=SC2016 # Keep GitHub expressions literal in refusal controls.
for mutation in \
'.w.on.workflow_dispatch={}' \
'.w.on.workflow_call.inputs["enable-recovery"].default=true' \
'.w.on.workflow_call.inputs["confirm-publication"].default=true' \
'.w.on.workflow_call.inputs.repository={type:"string"}' \
'.w.on.workflow_call={}' \
'.w.on.workflow_dispatch.inputs["enable-recovery"].default=true' \
'.w.on.workflow_dispatch.inputs["confirm-publication"].default=true' \
'.w.on.workflow_dispatch.inputs.repository={type:"string"}' \
'.w.jobs.recover.if="inputs.enable-recovery"' \
'.w.jobs.recover.permissions.contents="write"' \
'.w.jobs.recover.concurrency["cancel-in-progress"]=true' \
Expand All @@ -97,7 +108,15 @@ for mutation in \
'.c.jobs["test-publish-release-recovery-disabled"].with["enable-recovery"]=true' \
'.c.jobs["ci-required-checks"].needs |= map(select(. != "test-publish-release-recovery"))' \
'.c.jobs["ci-required-checks"].steps |= map(if .env.JOB_RESULTS then .env.JOB_RESULTS |= sub("needs.test-publish-release-recovery-disabled.result"; "") else . end)' \
'.c.jobs["test-create-release-config"].steps |= map(select(.run != "bash .github/tests/test-publish-release-recovery.sh"))'; do
'.c.jobs["test-create-release-config"].steps |= map(select(.run != "bash .github/tests/test-publish-release-recovery.sh"))' \
'.f.jobs.recover=.w.jobs.recover' \
'.f.on.workflow_call.secrets.APP_PRIVATE_KEY={required:false}' \
'.f.jobs["offline-test"].permissions.contents="write"' \
'.f.jobs["offline-test"].steps[1].env.CATALOGUE_SHA="main"' \
'.f.jobs["offline-test"].steps |= del(.[3])' \
'.f.jobs["offline-test"].steps[4].if="false"' \
'.f.on.workflow_call.inputs["offline-test"].default=true' \
'.c.jobs["test-publish-release-recovery"].uses="./.github/workflows/publish-release-recovery.yaml"'; do
jq "$mutation" "$work/bundle.json" > "$work/mutated.json"
if jq -e -f "$work/guard.jq" "$work/mutated.json" >/dev/null; then fail "accepted mutation: $mutation"; fi
done
Expand Down
29 changes: 29 additions & 0 deletions .github/tests/test-test-script-wiring.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,35 @@ blocked() {
}

run_guard || fail 'empty test directory should be accepted'
# Coverage must recognize real job-level calls in both same-repository forms,
# without treating a comment, step call or similarly named workflow as execution.
printf 'on:\n workflow_call: {}\njobs: {}\n' >"$work/repo/.github/workflows/native-fixture.yaml"
for reference in './.github/workflows/native-fixture.yaml' '$/.github/workflows/native-fixture.yaml'; do
# shellcheck disable=SC2016 # Preserve the required-result expression as fixture data.
REFERENCE="$reference" yq '.jobs.workflow-test = {"uses": strenv(REFERENCE)} |
.jobs.ci-required-checks.needs += ["workflow-test"] |
.jobs.ci-required-checks.steps[0].env.JOB_RESULTS += " ${{ needs.workflow-test.result }}"' \
"$work/base.yaml" >"$ci"
run_guard || fail "same-repository workflow call was not covered: $reference"
done
for mutation in \
'.jobs.workflow-test.uses += ".extra"' \
'.jobs.workflow-test.name = .jobs.workflow-test.uses | del(.jobs.workflow-test.uses)' \
'.jobs.workflow-test.steps = [{"uses": .jobs.workflow-test.uses}] | del(.jobs.workflow-test.uses)'; do
yq "$mutation" "$ci" >"$work/bad-call.yaml"
cp "$work/bad-call.yaml" "$ci"
printf '# uses: ./.github/workflows/native-fixture.yaml\n' >>"$ci"
if run_guard; then fail 'non-job or inexact workflow reference counted as coverage'; fi
grep -Fq 'native-fixture.yaml' "$work/result" || fail 'missing reusable-workflow diagnostic'
# shellcheck disable=SC2016 # The GitHub required-result expression stays literal fixture data.
REFERENCE='$/.github/workflows/native-fixture.yaml' yq '.jobs.workflow-test = {"uses": strenv(REFERENCE)} |
.jobs.ci-required-checks.needs += ["workflow-test"] |
.jobs.ci-required-checks.steps[0].env.JOB_RESULTS += " ${{ needs.workflow-test.result }}"' \
"$work/base.yaml" >"$ci"
done
rm "$work/repo/.github/workflows/native-fixture.yaml"
cp "$work/base.yaml" "$ci"
echo 'PASS: exact job-level classic/native workflow calls and three non-call controls'
touch "$work/repo/.github/tests/test-sentinel.sh"
blocked 'new test without a CI step'

Expand Down
Loading
Loading