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
6 changes: 5 additions & 1 deletion .github/workflows/gitignore-baseline-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# - test/scripts/sync-gitignore-baseline/**
# - test/scripts/compliance-remediate/**
# - tests/test_seed_repo_template_gitignore.bats
# - tests/test_seed_repo_template_workflows.bats
#
# Gates (all must pass before merge):
# 1. shellcheck — static analysis of the lib + sync + seed + remediate scripts
Expand All @@ -28,6 +29,7 @@ on:
- 'test/scripts/sync-gitignore-baseline/**'
- 'test/scripts/compliance-remediate/**'
- 'tests/test_seed_repo_template_gitignore.bats'
- 'tests/test_seed_repo_template_workflows.bats'
- '.github/workflows/gitignore-baseline-tests.yml'
push:
branches:
Expand All @@ -41,6 +43,7 @@ on:
- 'test/scripts/sync-gitignore-baseline/**'
- 'test/scripts/compliance-remediate/**'
- 'tests/test_seed_repo_template_gitignore.bats'
- 'tests/test_seed_repo_template_workflows.bats'
- '.github/workflows/gitignore-baseline-tests.yml'

permissions:
Expand Down Expand Up @@ -82,4 +85,5 @@ jobs:
test/scripts/lib/gitignore-baseline.bats \
test/scripts/sync-gitignore-baseline/ \
test/scripts/compliance-remediate/ \
tests/test_seed_repo_template_gitignore.bats
tests/test_seed_repo_template_gitignore.bats \
tests/test_seed_repo_template_workflows.bats
88 changes: 83 additions & 5 deletions scripts/seed-repo-template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
#
# Usage:
# STANDARDS_DIR=<path> bash scripts/seed-repo-template.sh --emit-baseline .gitignore
# STANDARDS_DIR=<path> bash scripts/seed-repo-template.sh --emit-workflow dev-lead.yml
#
# --emit-baseline .gitignore
# Print the seeded .gitignore: the marker-wrapped L1 secrets baseline on top
# (sourced from $STANDARDS_DIR/.gitignore), then standard ecosystem/OS L2 entries
# below the END marker. L2 must NOT re-ignore any path the baseline negates
# (e.g. .env.example is re-allowed in L1 via !.env.example).
#
# --emit-workflow <name.yml>
# Print the caller-stub workflow VERBATIM from $STANDARDS_DIR/standards/workflows/,
# the single source of truth for the org baseline scaffold. The re-seed MUST read
# the stub through this path — never from a stale embedded copy — so it can never
# revert repo-template to a wrong-channel pin (repo-template#86 flipped the
# major-scoped `@<agent>/v<M>-stable` tags back to the BARE `@<agent>/stable` tier
# tags, undoing the standards-deploy convergence and seeding new repos onto the
# wrong channel — petry-projects/.github#886). As a fail-loud guard, a stub whose
# first-party channel-ref pin is a bare `<agent>/<tier>` (missing the `v<M>-` major
# prefix the compliance audit requires) is REFUSED rather than emitted.
#
# Env:
# STANDARDS_DIR path to the petry-projects/.github checkout that holds the
# canonical /.gitignore and scripts/lib/gitignore-baseline.sh.
# Defaults to two levels above this script (the repo root).
# canonical /.gitignore, scripts/lib/gitignore-baseline.sh, and
# standards/workflows/. Defaults to the repo root above this script.
set -euo pipefail

STANDARDS_DIR="${STANDARDS_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)}"
Expand All @@ -23,12 +35,78 @@ STANDARDS_DIR="${STANDARDS_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." &&
source "${STANDARDS_DIR}/scripts/lib/gitignore-baseline.sh"

_usage() {
echo "Usage: $0 --emit-baseline <file>" >&2
echo " Supported files: .gitignore" >&2
echo "Usage: $0 --emit-baseline <file> | --emit-workflow <name.yml>" >&2
echo " --emit-baseline supported files: .gitignore" >&2
echo " --emit-workflow reads standards/workflows/<name.yml> verbatim" >&2
exit 2
}

[ $# -ge 2 ] && [ "$1" = "--emit-baseline" ] || _usage
# _assert_vform_pins <file> — a caller stub delegating to a first-party reusable
# through a channel ref MUST pin the major-scoped `<agent>/v<M>-<tier>` form, never
# the bare `<agent>/<tier>`. Those lines carry the inline S7637 marker, so we scope
# the check to them (mirrors test/scripts/standards-templates/vform-pins.bats).
# Exits non-zero listing every offending pin so the re-seed fails loud.
_assert_vform_pins() {
local file="$1" bad unmarked agentref_bad
# Fail-closed (#887 / qodo): the marker-based check below only inspects lines
# carrying the '# NOSONAR(githubactions:S7637) first-party channel ref' marker.
# If a deployable template has a first-party reusable `uses:` line but that line
# LOST the marker, the pin check would see zero lines and pass — emitting an
# unvalidated (possibly bare) pin. So first refuse any first-party reusable
# `uses:` line that lacks the marker: the guard cannot validate what it can't
# see, and must fail closed rather than emit unvalidated. (Local `./` self-host
# refs and third-party actions are not '<...>-reusable.yml@' and are exempt.)
unmarked="$(grep -E '^[[:space:]]*uses:[[:space:]]*petry-projects/[^[:space:]]*-reusable\.yml@' "$file" 2>/dev/null \
| grep -vE 'S7637\) first-party channel ref' || true)"
if [ -n "$unmarked" ]; then
echo "::error::refusing to emit '${file#"${STANDARDS_DIR}"/}': first-party reusable uses: line(s) missing the '# NOSONAR(githubactions:S7637) first-party channel ref' marker — cannot validate the channel pin (fail-closed):" >&2
printf '%s\n' "$unmarked" | sed 's/^/ /' >&2
exit 3
fi
bad="$(grep -E 'S7637\) first-party channel ref' "$file" 2>/dev/null \
| sed -E 's/.*-reusable\.yml@([^[:space:]]+).*/\1/' \
| grep -vE '^[a-z0-9-]+/v[0-9]+-(stable|next|ring[0-9]+)$' || true)"
Comment thread
don-petry marked this conversation as resolved.
if [ -n "$bad" ]; then
echo "::error::refusing to emit '${file#"${STANDARDS_DIR}"/}': bare channel pin(s) — must be major-scoped <agent>/v<M>-<tier>:" >&2
printf '%s\n' "$bad" | sed 's/^/ /' >&2
exit 3
fi
# Same fail-closed guard for `with: agent_ref:` (CodeRabbit): dev-lead.yml /
# add-to-project.yml thread the SAME channel into the reusable via agent_ref, so
# a partial edit could drop it to a bare tier while the marked `uses:` pin stays
# valid. Templates without an agent_ref match nothing here and pass unaffected.
agentref_bad="$(grep -E '^[[:space:]]*agent_ref:[[:space:]]' "$file" 2>/dev/null \
| sed -E 's/.*agent_ref:[[:space:]]*([^[:space:]#]+).*/\1/' \
| grep -vE '^[a-z0-9-]+/v[0-9]+-(stable|next|ring[0-9]+)$' || true)"
if [ -n "$agentref_bad" ]; then
echo "::error::refusing to emit '${file#"${STANDARDS_DIR}"/}': agent_ref value(s) not major-scoped <agent>/v<M>-<tier>:" >&2
printf '%s\n' "$agentref_bad" | sed 's/^/ /' >&2
exit 3
fi
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# _emit_workflow <name.yml> — verbatim copy of the standards/workflows/ template,
# gated by the v-form pin guard above.
_emit_workflow() {
local name="$1" src="${STANDARDS_DIR}/standards/workflows/$1"
case "$name" in
*/*|.*|"") echo "::error::invalid workflow name '${name}'" >&2; exit 2 ;;
esac
if [ ! -f "$src" ]; then
echo "::error::unknown workflow '${name}' — no template at standards/workflows/${name}" >&2
exit 2
fi
_assert_vform_pins "$src"
cat "$src"
}

[ $# -ge 2 ] || _usage

case "$1" in
--emit-workflow) _emit_workflow "$2"; exit 0 ;;
--emit-baseline) : ;; # handled below
*) _usage ;;
esac

file="$2"
case "$file" in
Expand Down
23 changes: 21 additions & 2 deletions test/scripts/compliance-remediate/gitignore-baseline.bats
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,16 @@ case "$sub" in
else
exit "${GH_REF_EXISTS_RC:-0}"
fi ;;
*contents/.gitignore"?"ref=*) printf '%s' "${GH_FILE_SHA:-}" ;;
*contents/.gitignore"?"ref=*)
# Branch-sha lookup. A real 200 always carries a sha; an absent file is a
# 404. The shared lib (#865) treats GET-success-with-blank-sha as a hard
# error (sha-unresolved), so the absent case MUST 404 (→ create path),
# not print empty and exit 0.
if [ -n "${GH_FILE_SHA:-}" ]; then
printf '%s' "$GH_FILE_SHA"
else
printf 'gh: Not Found (HTTP 404)\n' >&2; exit 1
fi ;;
*contents/.gitignore)
# PUT of the upserted file onto the sync branch.
if printf '%s' "$args" | grep -q -- '--method PUT'; then
Expand All @@ -72,7 +81,17 @@ case "$sub" in
printf 'gh: Not Found (HTTP 404)\n' >&2
exit 1 # 404 — .gitignore absent
fi ;;
repos/*) printf '%s' "${GH_DEFAULT_BRANCH:-main}" ;;
repos/*)
# The repo endpoint serves BOTH the default-branch lookup and the
# preflight write probe (--jq .permissions.push, added to the shared
# sd_deploy_files_via_pr lib in #865). Differentiate on the jq arg so the
# write probe returns a boolean, not the default-branch string, else the
# preflight reads "main" != "true" and fails as no-write-access.
if printf '%s' "$args" | grep -q 'permissions.push'; then
printf '%s' "${GH_CAN_PUSH:-true}"
else
printf '%s' "${GH_DEFAULT_BRANCH:-main}"
fi ;;
esac
;;
esac
Expand Down
149 changes: 149 additions & 0 deletions tests/test_seed_repo_template_workflows.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env bats
# Tests for the repo-template WORKFLOW-STUB generator in
# scripts/seed-repo-template.sh (#886).
#
# The repo-template baseline re-seed ("chore: seed org baseline file scaffold")
# must source its workflow stubs from the canonical standards/workflows/ — which
# pin the major-scoped `<agent>/v<M>-stable` channel and carry the S7637 (uses:)
# and S7635 (secrets: inherit) NOSONAR markers — rather than from a stale
# embedded copy. When it drifted to an embedded copy it re-pinned repo-template
# back to the BARE `<agent>/stable` tier tag (repo-template#86: v1-stable →
# stable, v2-stable → stable), undoing the standards-deploy convergence and
# seeding every new repo onto the wrong channel.
#
# `--emit-workflow <name.yml>` copies standards/workflows/<name.yml> VERBATIM and
# refuses to emit a stub whose first-party channel-ref pin is a bare
# `<agent>/<tier>` (no v<M>- major prefix), so a regression fails loud instead of
# being re-seeded fleet-wide.
#
# STANDARDS_DIR points the seed script at this checkout so templates are read
# from the local standards/workflows/ instead of the network.

setup() {
REPO_ROOT="$(cd -- "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
SEED="${REPO_ROOT}/scripts/seed-repo-template.sh"
WF_DIR="${REPO_ROOT}/standards/workflows"
export STANDARDS_DIR="$REPO_ROOT"
}

emit() { STANDARDS_DIR="$REPO_ROOT" bash "$SEED" --emit-workflow "$1"; }

# The stubs the re-seed corrupted in repo-template#86 (the "spot-check" set).
RESEEDED_STUBS="dev-lead.yml auto-rebase.yml dependabot-automerge.yml"

@test "--emit-workflow dev-lead.yml is byte-identical to the standards template" {
run emit dev-lead.yml
[ "$status" -eq 0 ]
[ "$output" = "$(cat "${WF_DIR}/dev-lead.yml")" ]
}

@test "emitted dev-lead stub pins @dev-lead/v1-stable, never the bare tier tag" {
run emit dev-lead.yml
[ "$status" -eq 0 ]
echo "$output" | grep -qF 'dev-lead-reusable.yml@dev-lead/v1-stable'
echo "$output" | grep -qF 'agent_ref: dev-lead/v1-stable'
# The exact regression from repo-template#86 must be absent.
! echo "$output" | grep -qE '@dev-lead/stable([[:space:]]|$)'
! echo "$output" | grep -qE 'agent_ref:[[:space:]]*dev-lead/stable([[:space:]]|$)'
}

@test "every re-seeded stub emits a major-scoped v<M>-stable pin (no bare tier)" {
local name ref
for name in $RESEEDED_STUBS; do
run emit "$name"
[ "$status" -eq 0 ]
# Extract the @<ref> from the first-party channel-ref (S7637-marked) uses line.
ref="$(echo "$output" | grep -E 'S7637\) first-party channel ref' \
| sed -E 's/.*-reusable\.yml@([^[:space:]]+).*/\1/')"
[ -n "$ref" ] || { echo "no channel-ref line in $name"; return 1; }
echo "$ref" | grep -qE '^[a-z0-9-]+/v[0-9]+-stable$' \
|| { echo "$name pins non-v-form '$ref'"; return 1; }
done
}

@test "every re-seeded stub keeps its S7637 (uses:) and S7635 (secrets: inherit) markers" {
local name
for name in $RESEEDED_STUBS; do
run emit "$name"
[ "$status" -eq 0 ]
echo "$output" | grep -qF 'NOSONAR(githubactions:S7637)' \
|| { echo "$name lost its S7637 marker"; return 1; }
echo "$output" | grep -qE '^[[:space:]]*secrets:[[:space:]]+inherit[[:space:]]+# NOSONAR\(githubactions:S7635\)' \
|| { echo "$name lost its S7635 secrets: inherit marker"; return 1; }
done
}

@test "--emit-workflow rejects an unknown workflow name" {
run emit no-such-workflow.yml
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'unknown workflow'
}

@test "--emit-workflow rejects path-traversal, dotfile, and empty names before any FS access" {
# The name guard (case '*/*|.*|"') blocks directory traversal and dotfiles up
# front, so --emit-workflow can never read outside standards/workflows/.
run emit '../etc/passwd'
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'invalid workflow'

run emit 'foo/bar.yml'
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'invalid workflow'

run emit '.hidden.yml'
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'invalid workflow'

run emit ''
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'invalid workflow'
}

@test "--emit-workflow refuses a template carrying a bare <agent>/<tier> pin" {
# Fixture: a standards dir whose dev-lead template regressed to the bare pin.
local fixture="${BATS_TEST_TMPDIR}/fixture"
Comment thread
don-petry marked this conversation as resolved.
mkdir -p "${fixture}/standards/workflows" "${fixture}/scripts/lib"
cp "${REPO_ROOT}/scripts/lib/gitignore-baseline.sh" "${fixture}/scripts/lib/"
sed -E 's#@dev-lead/v[0-9]+-stable#@dev-lead/stable#g' \
"${WF_DIR}/dev-lead.yml" > "${fixture}/standards/workflows/dev-lead.yml"
run env STANDARDS_DIR="$fixture" bash "$SEED" --emit-workflow dev-lead.yml
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'bare'
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@test "--emit-workflow fails CLOSED when a first-party reusable uses: line lost its S7637 marker" {
# Fixture: a template whose dev-lead uses: line keeps a valid v-form pin but
# DROPPED the S7637 marker. The marker-based pin check would then inspect zero
# lines and pass (fail-open, #887/qodo). The guard must refuse instead.
local fixture="${BATS_TEST_TMPDIR}/fixture-nomarker"
mkdir -p "${fixture}/standards/workflows" "${fixture}/scripts/lib"
cp "${REPO_ROOT}/scripts/lib/gitignore-baseline.sh" "${fixture}/scripts/lib/"
sed -E '/uses:.*-reusable\.yml@/ s/[[:space:]]*# NOSONAR\(githubactions:S7637\).*$//' \
"${WF_DIR}/dev-lead.yml" > "${fixture}/standards/workflows/dev-lead.yml"
# Sanity: the pin itself is still valid v-form (only the marker was removed).
grep -qE 'uses:.*@dev-lead/v[0-9]+-stable[[:space:]]*$' "${fixture}/standards/workflows/dev-lead.yml"
run env STANDARDS_DIR="$fixture" bash "$SEED" --emit-workflow dev-lead.yml
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'marker'
}

@test "--emit-workflow fails CLOSED when agent_ref regressed to a bare tier (valid uses: pin)" {
# Fixture: dev-lead template keeps a valid v-form `uses:` pin + its S7637 marker
# but its `with: agent_ref:` dropped to the bare tier. The uses-pin check alone
# would pass; the agent_ref guard (CodeRabbit) must refuse instead.
local fixture="${BATS_TEST_TMPDIR}/fixture-agentref"
mkdir -p "${fixture}/standards/workflows" "${fixture}/scripts/lib"
cp "${REPO_ROOT}/scripts/lib/gitignore-baseline.sh" "${fixture}/scripts/lib/"
sed -E 's#^([[:space:]]*agent_ref:[[:space:]]*)dev-lead/v[0-9]+-stable#\1dev-lead/stable#' \
"${WF_DIR}/dev-lead.yml" > "${fixture}/standards/workflows/dev-lead.yml"
# Sanity: the uses: pin is untouched (still valid v-form + S7637 marker).
grep -qE 'uses:.*@dev-lead/v[0-9]+-stable.*S7637' "${fixture}/standards/workflows/dev-lead.yml"
run env STANDARDS_DIR="$fixture" bash "$SEED" --emit-workflow dev-lead.yml
[ "$status" -ne 0 ]
echo "$output" | grep -qi 'agent_ref'
}

@test "usage is shown when --emit-workflow is given no name" {
run bash "$SEED" --emit-workflow
[ "$status" -ne 0 ]
}
Loading