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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,40 @@ jobs:
echo "No shell scripts found — skipping"
fi

# Informational self-validation of THIS repo's canonical AGENTS.md against the
# standalone structural linter (scripts/agents-md-lint.sh, Phase 2 of epic
# #642). Phase 3 (#646) runs it on every PR in the linter's default
# INFORMATIONAL mode: each structural finding surfaces as a GitHub ::warning::
# annotation, but the job never fails the build. This job is deliberately NOT a
# branch-protection required status check — promotion to a blocking/required
# check (--mode failing + branch protection) is deferred to Phase 4. Do NOT add
# it to branch protection or pass --mode failing while it remains informational.
# See test/workflows/ci/agents-md-selfcheck.bats.
agents-md-selfcheck:
name: AGENTS.md Self-Check (informational)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Lint canonical AGENTS.md (informational)
# Default (informational) mode always exits 0 — structural findings
# surface as ::warning:: annotations without failing the build, mirroring
# run-bats.sh's non-fatal-signal convention. Do NOT pass --mode failing:
# promoting this to a blocking/required check is Phase 4 (see job header).
run: |
bash scripts/agents-md-lint.sh AGENTS.md \
| while IFS=$'\t' read -r severity rule line message; do
[ -n "$rule" ] || continue
if [ "$line" = "-" ] || [ -z "$line" ]; then
echo "::warning file=AGENTS.md::[${severity}] ${rule}: ${message}"
else
echo "::warning file=AGENTS.md,line=${line}::[${severity}] ${rule}: ${message}"
fi
done

agent-security:
name: Agent Security Scan
runs-on: ubuntu-latest
Expand Down
102 changes: 102 additions & 0 deletions test/workflows/ci/agents-md-selfcheck.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bats
# Regression guard for the informational AGENTS.md self-check wired into
# .github/workflows/ci.yml (issue #646, Phase 3 of epic #642).
#
# Phase 3 runs the standalone structural linter (scripts/agents-md-lint.sh)
# against THIS repo's own canonical AGENTS.md on every pull request, in
# INFORMATIONAL mode: it annotates each structural finding with a GitHub
# ::warning:: but must never fail the build (promotion to a failing/required
# check is deferred to Phase 4). These tests pin that contract so a future edit
# cannot silently (a) drop the self-check, (b) point it at the wrong file, or
# (c) flip it to failing mode and turn it into a de-facto blocking gate before
# Phase 4 sign-off.

load 'helpers/setup'

# Emit the `run:` step block whose text contains the given marker substring.
# Steps in ci.yml start at a 6-space "- " (the `steps:` child indent); every
# deeper-indented line belongs to that step. (Mirrors secret-scan-config.bats.)
step_block_with() {
local marker="$1"
local block="" out="" line=""
while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}"
if [[ "$line" == " - "* ]]; then
if [[ "$block" == " - "* ]]; then
[[ "$block" == *"$marker"* ]] && out="$block"
fi
block="$line"$'\n'
elif [[ "$line" =~ ^[[:space:]]{8} ]] || [[ "$line" =~ ^[[:space:]]*$ ]]; then
if [[ -n "$block" ]]; then
block+="$line"$'\n'
fi
else
if [[ "$block" == " - "* ]]; then
[[ "$block" == *"$marker"* ]] && out="$block"
fi
block=""
fi
Comment thread
don-petry marked this conversation as resolved.
done < "$TT_WORKFLOW"
if [[ "$block" == " - "* ]]; then
[[ "$block" == *"$marker"* ]] && out="$block"
fi
printf '%s' "$out"
}

@test "agents-md-selfcheck: the ci workflow exists" {
[[ -f "$TT_WORKFLOW" ]]
}

@test "agents-md-selfcheck: the linter script it invokes exists and is executable" {
# The self-check runs the Phase-2 standalone linter; if the path drifts the
# step would fail with file-not-found and red an otherwise-informational job.
[[ -x "${TT_REPO_ROOT}/scripts/agents-md-lint.sh" ]]
}

@test "agents-md-selfcheck: ci runs the structural linter against this repo's AGENTS.md" {
local block code
block="$(step_block_with 'scripts/agents-md-lint.sh')"
[[ -n "$block" ]]
# Strip comments so a comment mentioning AGENTS.md cannot satisfy the assertion.
code="$(printf '%s\n' "$block" | grep -vE '^[[:space:]]*#')"
[[ "$code" == *"AGENTS.md"* ]]
}

@test "agents-md-selfcheck: the self-check stays informational (no --mode failing)" {
# Informational default mode always exits 0; passing --mode failing would turn
# a REQUIRED-level finding into a build failure — that promotion is Phase 4.
# Strip comment lines first: a header comment saying "do NOT pass --mode
# failing" is good documentation and must not trip this assertion, which is
# about the actual invocation.
local block code
block="$(step_block_with 'scripts/agents-md-lint.sh')"
[[ -n "$block" ]]
code="$(printf '%s\n' "$block" | grep -vE '^[[:space:]]*#')"
# Verify the linter is invoked with AGENTS.md as its argument, not just mentioned.
[[ "$code" == *"scripts/agents-md-lint.sh"*"AGENTS.md"* ]]
[[ "$code" != *"--mode failing"* ]]
[[ "$code" != *"--mode=failing"* ]]
Comment thread
don-petry marked this conversation as resolved.
}

@test "agents-md-selfcheck: findings surface as ::warning:: annotations" {
# Mirror run-bats.sh's non-fatal-signal convention: findings annotate without
# failing the build.
local block
block="$(step_block_with 'scripts/agents-md-lint.sh')"
[[ -n "$block" ]]
[[ "$block" == *"::warning"* ]]
}

@test "agents-md-selfcheck: the self-check itself documents its informational, deferred-to-Phase-4 status" {
# AC #3: no new branch-protection required check while informational. The
# self-check's OWN step block must document that it is informational and that
# promotion to a blocking/required check is deferred to Phase 4, so a reviewer
# editing it later knows the non-blocking behaviour is deliberate. Scoping the
# assertion to the step block (not the whole file) keeps it from passing on an
# unrelated "informational" comment elsewhere in ci.yml.
local block
block="$(step_block_with 'scripts/agents-md-lint.sh')"
[[ -n "$block" ]]
[[ "$block" == *"informational"* ]]
[[ "$block" == *"Phase 4"* ]]
}
Loading