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
1 change: 1 addition & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
tests/test_lint_main_push_trigger.bats \
tests/caller_stub_freeze.bats \
tests/test_sc2_self_review_channel.bats \
tests/test_channel_pin_consistency.bats \
tests/pr_review_canary.bats \
tests/dev-lead/unit/test_maintainer_review_thread_gate.bats \
tests/dev-lead/unit/test_maintainer_comment_gate.bats \
Expand Down Expand Up @@ -266,7 +267,7 @@
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# NOTE: persist-credentials is intentionally left at the default (true) —

Check warning on line 270 in .github/workflows/lint.yml

View workflow job for this annotation

GitHub Actions / Lint

270:9 [comments-indentation] comment not indented like content
# vci_resolve_reusable needs `git fetch origin` to authenticate against this
# private repo when resolving same-repo channel tags. Setting
# persist-credentials: false would silently push every such caller into the
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-review-mention.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
# • This file is a THIN CALLER STUB. All review-dispatch logic lives in the
# reusable workflow above.
# • You MUST NOT change: the `uses:` ref — it is pinned to the
# `pr-review-mention/v2-stable` channel, a moving tag advanced centrally.
# `pr-review-mention/v2-next` channel, a moving tag advanced centrally.
# Never repoint it to `@main`, a SHA, or a frozen `@vX` (see
# ci-standards.md → Reusable workflow versioning). Also do not change the
# trigger events or the job-level `permissions:` block — reusable workflows
Expand Down
130 changes: 130 additions & 0 deletions scripts/lib/channel-pin-consistency.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# channel-pin-consistency.sh — same-file channel-pin/comment consistency guard
# (#1866 AC #3; regression guard for #1819).
#
# The workflow-stub sync generator (standards-deploy.yml, in the PUBLIC
# petry-projects/.github repo) PRESERVES each destination repo's own per-repo
# `uses:` channel pin. #1819 exposed the receiving-side hazard: a guard COMMENT
# (or the `agent_ref:` input) that names a channel can drift out of step with the
# actual `uses:` pin in the SAME file — moving one without the other. This library
# provides the receiving-side property this repo can enforce for its own committed
# stubs: any channel a caller stub's comments / `agent_ref` name for its OWN
# reusable must equal the `uses:` pin in the same file.
#
# It is a member of the receiving-guard family (validate-caller-inputs #1253,
# caller-stub-freeze #1255, sync-scope-guard #1700, self-review-channel SC2 #1624).
# Intentional CONTRAST references (a promotion target, a legacy form, a different
# agent's duty) are excluded so they are not mistaken for a stale pin comment.
#
# Pure helpers, no side effects, no network. This file defines functions only (no
# top-level `set -euo pipefail`), matching the repo's sourceable-lib convention
# (e.g. scripts/lib/self-review-channel.sh, which it sources for the shared
# `src_stub_uses_ref` / `src_stub_agent_ref` parsers).
#
# Sourced by tests/test_channel_pin_consistency.bats.

CPC_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/lib/self-review-channel.sh
source "${CPC_LIB_DIR}/self-review-channel.sh"

# Contrast markers: words that flag a comment line as INTENTIONALLY naming a
# channel other than this file's own pin — a promotion target, a legacy/pre-#1184
# form, a cross-agent duty note, or an explicit dogfood/canary contrast — so it is
# not mistaken for a stale/inconsistent pin comment.
CPC_CONTRAST_RE='legacy|promote|promoted|promotion|fleet|pre-#|independent|on purpose|instead|rather than|formerly|dogfood'

# cpc_channel_refs
# Read text on stdin; emit each `<agent>/<channel>` channel ref it contains, one
# per line (deduped, in first-seen order). A channel ref is `<agent>/(vN-)?<tier>`
# with tier ∈ {stable, next, ring<N>}; the agent token is lowercase/hyphenated.
# A full `…/<workflow>.yml@<agent>/<channel>` token yields just `<agent>/<channel>`
# (the `.` in `.yml` bounds the agent). Bare tiers with no agent and ordinary
# paths yield nothing. Always returns 0.
cpc_channel_refs() {
grep -oE '[a-z][a-z0-9-]*/(v[0-9]+-)?(stable|next|ring[0-9]+)([^a-z0-9-]|$)' \
| sed -E 's/[^a-z0-9-]$//' \
| awk 'NF && !seen[$0]++'
}

# cpc_line_is_contrast <text>
# Return 0 iff <text> carries a contrast marker (see CPC_CONTRAST_RE).
cpc_line_is_contrast() {
printf '%s' "$1" | grep -qiE "$CPC_CONTRAST_RE"
}

# cpc_stub_conflicts <file>
# Print each same-file channel-pin conflict in <file>, one per line; return 1 if
# any are found, else 0. Two conflict classes:
# (a) a COMMENT names this file's OWN agent at a channel != the `uses:` pin,
# on a line that is not a contrast reference;
# (b) the `agent_ref:` input names a channel != the `uses:` pin.
# A file with no reusable `uses:` channel pin has nothing to check → returns 0
# with no output (not-applicable).
cpc_stub_conflicts() {
local file="$1" pinref pinagent pinchan aref line comment ref agent chan found=0
pinref="$(src_stub_uses_ref "$file")" || return 0 # no pin → N/A
case "$pinref" in
*/*) ;; # need an <agent>/<channel> pin
*) return 0 ;; # e.g. @main / a SHA → N/A
esac
pinagent="${pinref%/*}"
pinchan="${pinref##*/}"

# (a) comment/pin consistency.
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
*"#"*) comment="${line#*#}" ;; # text after the first '#'
*) continue ;;
esac
cpc_line_is_contrast "$comment" && continue
while IFS= read -r ref; do
agent="${ref%/*}"
chan="${ref##*/}"
if [ "$agent" = "$pinagent" ] && [ "$chan" != "$pinchan" ]; then
printf 'comment names channel `%s` but `uses:` pins `%s`\n' "$ref" "$pinref"
found=1
fi
done < <(printf '%s\n' "$comment" | cpc_channel_refs)
done < "$file"

# (b) agent_ref/uses consistency (only when an agent_ref input is present).
if aref="$(src_stub_agent_ref "$file")"; then
if [ "$aref" != "$pinref" ]; then
printf '`agent_ref: %s` does not match the `uses:` pin `%s`\n' "$aref" "$pinref"
found=1
fi
fi

return "$found"
}

# cpc_assert_consistent <file> [<label>]
# End-to-end AC #3 guard. Passes (0) a file with no channel pin (not-applicable)
# or one whose comments AND `agent_ref` agree with the `uses:` pin. On conflict,
# emits an actionable message naming issues #1866/#1819 and the offending refs to
# stderr, and returns 1 — steering a contributor to fix the COMMENT/`agent_ref`
# to match the preserved pin rather than repinning `uses:` to match the comment.
cpc_assert_consistent() {
local file="$1" label="${2:-$1}" conflicts
if ! src_stub_uses_ref "$file" >/dev/null 2>&1; then
printf 'channel-pin consistency: %s has no reusable channel pin — N/A\n' "$label"
return 0
fi
if conflicts="$(cpc_stub_conflicts "$file")"; then
printf 'channel-pin consistency: %s — comments, `agent_ref`, and `uses:` pin agree — OK\n' "$label"
return 0
fi
{
printf '::error::channel-pin INCONSISTENCY in %s (issue #1866, regression of #1819):\n' "$label"
while IFS= read -r c; do
[ -n "$c" ] && printf ' - %s\n' "$c"
done <<<"$conflicts"
printf 'A guard comment or `agent_ref` in a caller stub must name the SAME channel as the\n'
printf '`uses:` pin in the same file. The standards-sync generator preserves THIS repo'"'"'s own\n'
printf '`uses:` channel pin, so a comment (or agent_ref) naming a different channel is stale —\n'
printf 'fix the COMMENT / `agent_ref` to match the pin; do NOT repin `uses:` to match the\n'
printf 'comment. If a line is an intentional contrast reference (a promotion target, a legacy\n'
printf 'form, or a different agent'"'"'s duty), mark it (e.g. "promoted", "legacy", "fleet"). See #1866.\n'
} >&2
return 1
}
12 changes: 12 additions & 0 deletions tests/fixtures/channel-pin/stub-agent-ref-desync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Fixture: `uses:` and `agent_ref:` name DIFFERENT channels in the same file — a
# same-file pin desync the guard must catch even though the header comment agrees
# with the `uses:` pin. (The reusable checks its own scripts out from agent_ref,
# so a stable `uses:` driven by a `next` agent_ref runs unsafe logic.)
# • It is pinned to the `demo/v1-stable` channel.
name: Demo
on: [pull_request]
jobs:
demo:
uses: petry-projects/.github/.github/workflows/demo-reusable.yml@demo/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
with:
agent_ref: demo/v1-next
11 changes: 11 additions & 0 deletions tests/fixtures/channel-pin/stub-aligned.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Fixture: a caller stub whose guard comment names the SAME channel as the pin,
# with an `agent_ref` that also matches. Nothing to flag.
# • You MUST NOT change the `uses:` ref — it is pinned to the `demo/v2-next`
# channel, a moving tag advanced centrally.
name: Demo
on: [pull_request]
jobs:
demo:
uses: petry-projects/.github/.github/workflows/demo-reusable.yml@demo/v2-next # NOSONAR(githubactions:S7637) first-party channel ref
with:
agent_ref: demo/v2-next
8 changes: 8 additions & 0 deletions tests/fixtures/channel-pin/stub-both-differ.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Fixture (#1866 AC #6): the comment names a channel that differs from the `uses:`
# pin in BOTH tier and major version — both must be caught, not just the tier.
# • It is pinned to the `demo/v1-stable` channel — the moving tag.
name: Demo
on: [pull_request]
jobs:
demo:
uses: petry-projects/.github/.github/workflows/demo-reusable.yml@demo/v2-next # NOSONAR(githubactions:S7637) first-party channel ref
10 changes: 10 additions & 0 deletions tests/fixtures/channel-pin/stub-comment-mismatch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Fixture: the #1819 defect shape — the guard comment names one channel while
# the `uses:` pin names another. The pin is canonical; the stale comment is the
# inconsistency AC #3 must catch.
# • You MUST NOT change the `uses:` ref — it is pinned to the `demo/v2-stable`
# channel, a moving tag advanced centrally.
name: Demo
on: [pull_request]
jobs:
demo:
uses: petry-projects/.github/.github/workflows/demo-reusable.yml@demo/v2-next # NOSONAR(githubactions:S7637) first-party channel ref
11 changes: 11 additions & 0 deletions tests/fixtures/channel-pin/stub-contrast.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Fixture: an intentional CONTRAST reference must NOT be flagged. This stub pins
# the `next` canary channel on purpose and its prose names the `stable` channel
# only to describe the promotion target — a contrast marker ("promoted"/"fleet")
# tells the guard this is deliberate.
# • This repo dogfoods `demo/v1-next`; it is promoted to `demo/v1-stable`
# (the fleet) only after the canary proves healthy.
name: Demo
on: [pull_request]
jobs:
demo:
uses: petry-projects/.github/.github/workflows/demo-reusable.yml@demo/v1-next # NOSONAR(githubactions:S7637) first-party channel ref
10 changes: 10 additions & 0 deletions tests/fixtures/channel-pin/stub-no-pin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Fixture: a plain workflow with NO reusable channel pin. There is no `uses:`
# channel ref to be inconsistent with, so the guard treats it as not-applicable
# and passes cleanly (this mirrors non-caller workflows in the live loop).
name: Plain
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hi
12 changes: 12 additions & 0 deletions tests/fixtures/channel-pin/stub-other-agent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Fixture: a comment naming a DIFFERENT agent's channel (`other/v1-next`) is not
# this file's own pin and must not be flagged — cross-agent duty notes are common.
# The exclusion here is by AGENT mismatch alone (no contrast marker on the line).
# • It is pinned to the `demo/v1-stable` channel.
# • The related canary duty lives in `other/v1-next` (a different agent).
name: Demo
on: [pull_request]
jobs:
demo:
uses: petry-projects/.github/.github/workflows/demo-reusable.yml@demo/v1-stable # NOSONAR(githubactions:S7637) first-party channel ref
with:
agent_ref: demo/v1-stable
Loading
Loading