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
24 changes: 24 additions & 0 deletions .github/actions/affirmation-check/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: MPL-2.0
name: 'Affirmation Document Gate'
description: 'Validates an applicable AFFIRMATION snapshot without treating it as a universal or self-proving document.'
inputs:
required:
description: 'Set true only when the repository declares the governance-tier capability.'
required: false
default: 'false'
is-template:
description: >-
Set true for a template repository, whose AFFIRMATION.adoc is expected to
carry {{…}} placeholders. Defaults to the repository's own is_template
flag, so template repos are exempt without per-repo configuration.
required: false
default: ${{ github.event.repository.is_template }}
runs:
using: 'composite'
steps:
- name: Run Affirmation Check
shell: bash
env:
AFFIRMATION_REQUIRED: ${{ inputs.required }}
AFFIRMATION_IS_TEMPLATE: ${{ inputs.is-template }}
run: "${{ github.action_path }}/check.sh"
119 changes: 119 additions & 0 deletions .github/actions/affirmation-check/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
#
# Validates an AFFIRMATION snapshot against AFFIRMATION-STANDARD.adoc.
#
# This gate asks the question the standard asks. Three rules it must respect,
# each of which a previous revision got wrong:
#
# 1. The standard permits the affirmation at the repo ROOT *or* under docs/.
# The canonical skeleton in rsr-template-repo ships at docs/AFFIRMATION.adoc,
# so a root-only search reports a present affirmation as missing.
# 2. The standard BANS AFFIRMATION.md ("as README.md is banned"). Accepting it
# would make the guard permit what the standard forbids, so a .md file is
# an error naming the fix, not a silently-accepted candidate.
# 3. A template repository ships placeholders BY DESIGN. Without an exemption
# the gate fails exactly the repos whose job is to carry the template.

set -euo pipefail

root=${GITHUB_WORKSPACE:-.}
required=${AFFIRMATION_REQUIRED:-false}
is_template=${AFFIRMATION_IS_TEMPLATE:-false}
aff_file=

# Rule 2: a banned .md spelling is a named error, never a fallback.
for banned in AFFIRMATION.md docs/AFFIRMATION.md AFFIRMATION; do
if [[ -f "$root/$banned" ]]; then
echo "::error::$banned is banned by AFFIRMATION-STANDARD.adoc (AsciiDoc only, as README.md is banned). Rename it to ${banned%.md}.adoc."

Check warning on line 28 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmuo&open=AaCkX1GWVyryiNBpfmuo&pullRequest=806
exit 1
fi
done

# Rule 1: root OR docs/, in the standard's stated order of preference.
for candidate in AFFIRMATION.adoc docs/AFFIRMATION.adoc; do
if [[ -f "$root/$candidate" ]]; then
aff_file=$candidate
break
fi
done

if [[ -z "$aff_file" ]]; then
if [[ "$required" == "true" ]]; then
echo "::error::AFFIRMATION.adoc is required by the declared governance-tier capability (looked in ./ and ./docs/)."

Check warning on line 43 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmup&open=AaCkX1GWVyryiNBpfmup&pullRequest=806
exit 1
fi
echo "::notice::AFFIRMATION is not applicable: governance-tier was not required."
exit 0
fi

path=$root/$aff_file
echo "Found AFFIRMATION document: $aff_file"

substantive_lines=$(awk '!/^[[:space:]]*(#|\/\/|;|$)/ { count++ } END { print count + 0 }' "$path")
if (( substantive_lines < 5 )); then
echo "::error::$aff_file has only $substantive_lines substantive lines; it is a stub, not an affirmation."

Check warning on line 55 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmuq&open=AaCkX1GWVyryiNBpfmuq&pullRequest=806
exit 1
fi

# Rule 3: a template repo is MEANT to carry placeholders; everyone else is not.
placeholder_re='\{\{|TODO: update|<PROJECT|YOUR_PROJECT|lorem ipsum|example\.com|<full 40-character SHA|<ISO-8601|<YYYY-MM-DD'
if grep -qiE "$placeholder_re" "$path"; then
if [[ "$is_template" == "true" ]]; then
echo "::notice::$aff_file carries template placeholders; accepted because this repository is a template."
else
echo "::error::$aff_file contains template placeholders (unreplaced {{…}} or <…> anchor fields)."

Check warning on line 65 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmur&open=AaCkX1GWVyryiNBpfmur&pullRequest=806
exit 1
fi
fi

# The signature is the signature on the commit containing this content. Text
# such as "Signed:" inside the document proves nothing.
#
# Rule 4: `N` is a VERDICT, not a missing reading. `git log --format=%G?`
# returns `N` for "this commit carries no signature at all" -- which is a
# measured violation of the standard, whose own worked example is
# `git commit -S -s docs/AFFIRMATION.adoc`. A previous revision initialised
# this variable to `N` as its "nothing to report" sentinel, so the genuinely
# unsigned case shared an arm with the cases where no reading was possible and
# fell through to a `::notice::`. A notice cannot fail a job, so an unsigned
# affirmation passed a gate that exists to establish the affirmation is signed
# -- the self-refuting outcome the standard warns about.
#
# The three states are now distinct, because only one of them is unknowable:
# unavailable the working tree is not a Git repository at all
# "" Git is present but the file has no commit in the available
# history (a shallow checkout) -- honestly indeterminate
# N Git read the commit and it is unsigned -- a verdict, so fatal
signature=unavailable
last_update_ts=
if git -C "$root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
signature=$(git -C "$root" log -1 --format='%G?' -- "$aff_file" 2>/dev/null || true)
last_update_ts=$(git -C "$root" log -1 --format='%at' -- "$aff_file" 2>/dev/null || true)
fi

case "$signature" in
G) echo "Affirmation commit signature verified with a trusted key." ;;
U) echo "::notice::Affirmation commit has a valid signature from an untrusted or locally unknown key." ;;
X|Y) echo "::warning::Affirmation commit signature is valid but the signature or its key has expired." ;;
B|R|E) echo "::error::Affirmation commit signature is bad, revoked, or failed verification."; exit 1 ;;

Check warning on line 99 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmus&open=AaCkX1GWVyryiNBpfmus&pullRequest=806
N) echo "::error::Affirmation commit is UNSIGNED. AFFIRMATION-STANDARD.adoc requires a signed commit (git commit -S -s). An unsigned affirmation attests to nothing."; exit 1 ;;

Check warning on line 100 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmut&open=AaCkX1GWVyryiNBpfmut&pullRequest=806
"") echo "::notice::Affirmation commit is not in the available Git history (shallow checkout), so its signature could not be read. This is a limitation of the checkout, not a verdict; fetch full history to verify." ;;
unavailable) echo "::notice::Not a Git repository, so no commit signature could be read." ;;
*) echo "::error::Unrecognised commit signature status: ${signature}. Failing closed: a governance gate must not pass a status it cannot interpret."; exit 1 ;;

Check warning on line 103 in .github/actions/affirmation-check/check.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Redirect this error message to stderr (>&2).

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1GWVyryiNBpfmuu&open=AaCkX1GWVyryiNBpfmuu&pullRequest=806
esac

# A dated affirmation is a frozen receipt, not a claim that remains current
# forever. Report age without invalidating historical evidence or forcing an
# empty monthly rewrite.
if [[ -n "$last_update_ts" ]]; then
current_ts=$(date +%s)
age_days=$(( (current_ts - last_update_ts) / 86400 ))
if (( age_days > 28 )); then
echo "::warning::$aff_file is a $age_days-day-old snapshot; verify its anchor before relying on it as current."
else
echo "$aff_file snapshot age: $age_days days."
fi
fi

echo "AFFIRMATION document validation passed."
140 changes: 140 additions & 0 deletions .github/actions/affirmation-check/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
#
# Controls for the affirmation gate. Each control names the specific defect it
# would catch; a control that cannot fail against a broken gate proves nothing.

set -euo pipefail

here=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
fixture=$(mktemp -d)
trap 'rm -rf -- "$fixture"' EXIT

pass() { printf ' ok %s\n' "$1"; }
fail() { printf ' FAIL %s\n' "$1" >&2; exit 1; }

# `if` context is used so that set -e does not fire on an expected non-zero exit.
expect_pass() {
local desc=$1; shift
if env GITHUB_WORKSPACE="$fixture" "$@" "$here/check.sh" >/dev/null 2>&1
then pass "$desc"; else fail "$desc (expected exit 0)"; fi
}
expect_fail() {
local desc=$1; shift
if env GITHUB_WORKSPACE="$fixture" "$@" "$here/check.sh" >/dev/null 2>&1
then fail "$desc (expected non-zero exit)"; else pass "$desc"; fi
}

reset_fixture() { rm -rf -- "${fixture:?}"/* ; mkdir -p "$fixture/docs"; }

good_affirmation() {
printf '%s\n' \
'= AFFIRMATION — controlled fixture' \
'This snapshot makes a falsifiable claim.' \
'The claim is anchored to a named revision.' \
'Tests were run and their scope is stated.' \
'Unproved properties are not called proved.' \
'Later revisions must be assessed separately.' \
> "$1"

Check warning on line 38 in .github/actions/affirmation-check/test.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Assign this positional parameter to a local variable.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaCkX1QLVyryiNBpfmuv&open=AaCkX1QLVyryiNBpfmuv&pullRequest=806
}

echo 'affirmation-check controls'

# --- baseline behaviour ------------------------------------------------------
reset_fixture
expect_pass 'absent + not required passes' AFFIRMATION_REQUIRED=false
expect_fail 'absent + required fails' AFFIRMATION_REQUIRED=true

printf '= AFFIRMATION\n' > "$fixture/AFFIRMATION.adoc"
expect_fail 'stub fails' AFFIRMATION_REQUIRED=true

reset_fixture
good_affirmation "$fixture/AFFIRMATION.adoc"
expect_pass 'valid affirmation at root passes' AFFIRMATION_REQUIRED=true

# --- defect 1: the standard permits docs/, and the canonical rsr-template-repo
# skeleton ships at docs/AFFIRMATION.adoc. A root-only gate FAILS here.
reset_fixture
good_affirmation "$fixture/docs/AFFIRMATION.adoc"
expect_pass 'valid affirmation at docs/ passes' AFFIRMATION_REQUIRED=true

# --- defect 2: AFFIRMATION.md is banned by the standard. A gate that lists it
# as a fallback candidate FAILS both of these.
reset_fixture
good_affirmation "$fixture/AFFIRMATION.md"
expect_fail 'AFFIRMATION.md is rejected as banned' AFFIRMATION_REQUIRED=false

reset_fixture
good_affirmation "$fixture/docs/AFFIRMATION.md"
expect_fail 'docs/AFFIRMATION.md is rejected as banned' AFFIRMATION_REQUIRED=false

# --- placeholders ------------------------------------------------------------
reset_fixture
good_affirmation "$fixture/AFFIRMATION.adoc"
printf '= AFFIRMATION — {{PROJECT_NAME}}\n' >> "$fixture/AFFIRMATION.adoc"
expect_fail 'placeholders fail a normal repo' AFFIRMATION_REQUIRED=true

# --- defect 3: a template repo ships placeholders by design.
# A gate with no exemption FAILS here.
expect_pass 'placeholders are exempt in a template repo' \
AFFIRMATION_REQUIRED=true AFFIRMATION_IS_TEMPLATE=true

# unreplaced anchor fields are placeholders too
reset_fixture
good_affirmation "$fixture/AFFIRMATION.adoc"
printf '| Commit (HEAD)\n| `<full 40-character SHA — never abbreviated>`\n' >> "$fixture/AFFIRMATION.adoc"
expect_fail 'unreplaced anchor field fails' AFFIRMATION_REQUIRED=true


# --- defect 4: an UNSIGNED commit is a VERDICT, not a missing reading --------
# A gate that initialises its signature variable to `N` as a "nothing to
# report" sentinel routes the genuinely-unsigned case into the same
# catch-all arm as the cases where no reading was possible, and emits a
# ::notice::. A notice cannot fail a job, so the previous revision PASSED
# an unsigned affirmation -- while existing on order to establish that the
# affirmation is signed. These controls separate the three states.
gitfix=$(mktemp -d)
trap 'rm -rf -- "$fixture" "$gitfix"' EXIT
mkdir -p "$gitfix/docs"
good_affirmation "$gitfix/docs/AFFIRMATION.adoc"
git -C "$gitfix" init -q
git -C "$gitfix" config user.name 'Control Fixture'
git -C "$gitfix" config user.email 'control@example.invalid'
git -C "$gitfix" config commit.gpgsign false
git -C "$gitfix" add -A

# Git is available, but the file has no commit in the available history -- the
# shallow-checkout case. Genuinely indeterminate, so it must NOT be fatal.
expect_pass 'uncommitted affirmation is indeterminate, not fatal' \
AFFIRMATION_REQUIRED=true GITHUB_WORKSPACE="$gitfix"

# The mutant-killer. Against the previous revision this control PASSES the gate
# and therefore FAILS the suite, which is what makes it a control rather than a
# decoration.
git -C "$gitfix" commit -q --no-gpg-sign -m 'control: unsigned affirmation'
expect_fail 'UNSIGNED affirmation commit is rejected' \
AFFIRMATION_REQUIRED=true GITHUB_WORKSPACE="$gitfix"

# --- positive control: a genuinely signed commit must still pass -------------
# A suite that only proves the gate can FAIL would be satisfied by a gate
# that rejects everything. The signing key is generated by this test, so
# the control needs no external secret and no key material in the repo.
keydir="$gitfix/.control-keys"
mkdir -p "$keydir"
ssh-keygen -q -t ed25519 -N '' -C 'affirmation-control' -f "$keydir/id" </dev/null
printf 'control@example.invalid %s\n' "$(cat "$keydir/id.pub")" > "$keydir/allowed_signers"
git -C "$gitfix" config gpg.format ssh
git -C "$gitfix" config user.signingkey "$keydir/id.pub"
git -C "$gitfix" config gpg.ssh.allowedSignersFile "$keydir/allowed_signers"

printf '\nA further controlled revision of the same claim.\n' >> "$gitfix/docs/AFFIRMATION.adoc"
git -C "$gitfix" add -A
git -C "$gitfix" commit -q -S -m 'control: signed affirmation'
# Assert the fixture really produced a trusted signature; otherwise the control
# below would pass for the wrong reason and prove nothing.
sig=$(git -C "$gitfix" log -1 --format='%G?' -- docs/AFFIRMATION.adoc)
[[ $sig == G ]] || fail "fixture did not produce a trusted signature (got '$sig')"
expect_pass 'signed affirmation commit passes' \
AFFIRMATION_REQUIRED=true GITHUB_WORKSPACE="$gitfix"

echo 'affirmation-check controls passed'
Loading