Skip to content
Open
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
137 changes: 96 additions & 41 deletions .github/workflows/dev-enforcement.yml
Original file line number Diff line number Diff line change
@@ -1,62 +1,117 @@
name: Dev Workflow Enforcement

# This gate had never run a single job. Every layer was broken independently,
# and each failed in a way that looked like the layer below it.
#
# 1. The YAML did not parse. In the pr-status job `steps:` was indented inside
# `if:`, and further down `run:` inside `if:`. GitHub compiled zero jobs and
# titled the run by its file path rather than by `name:` — which is what a
# red X on ".github/workflows/dev-enforcement.yml" means.
# 2. No job had an actions/checkout step, so `git log -1` ran outside a
# repository and .trinity/dev_session.json was never on disk.
# 3. `$COMMIT` was never assigned, so the issue-ID test read an empty string and
# could only ever print "Missing issue ID" — including for a correct commit.
# 4. The commit-format regex was `^\[a-f]+\(...`, which requires a literal `[`
# at the start of the subject. No conventional commit begins with one, and
# its result was piped to `head -1` and discarded regardless.
# 5. session-check gated CI on .trinity/dev_session.json being TESTED or
# COMMITTED. That is local working state; CI cannot observe a dev session,
# and on main it asserts whatever state happened to be committed.
#
# REPORTING, NOT BLOCKING — and that is a deliberate difference from the same
# gate in trinity-fpga, which does block on pull requests.
#
# Measured before choosing, on the last 60 non-merge commits of each main:
#
# trinity-fpga 3 / 60 ( 5%) would fail
# trinity 25 / 60 (42%) would fail
#
# This repository's practice is largely prose subjects, many in Russian
# ("Локализовать ветвь дерева технологий (#785)"), while CLAUDE.md documents
# `<type>(<scope>): <description>`. That gap is a policy question and not mine
# to settle by switching a gate to blocking: a check that fails 42% of the work
# is a check people route around. So it says what it sees, with counts, and the
# decision to tighten it stays with a reader who can also change the rule.

on:
pull_request:
types: [opened, synchronize]
push:
branches: [main]

permissions:
contents: read
pull-requests: read

jobs:
ci-check:
commit-format:
name: CI Validation
runs-on: ubuntu-latest
steps:
- name: Validate Commit Format
# Full history, unconditionally. There was no checkout at all before,
# which is why `git log` failed.
#
# The first version wrote
# fetch-depth: ${{ github.event_name == 'pull_request' && 0 || 1 }}
# which evaluates to 1 on a pull request, not 0: `true && 0` is `0`, `0`
# is falsy, so `|| 1` wins. The clone came out shallow and the job died
# with `fatal: Invalid revision range <base>..<head>` — a range error that
# reads as a bad SHA rather than as a missing object. The ternary idiom is
# only safe when the middle operand is truthy.
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Commit subject format
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
git log -1 --format=%s | grep -E '^\[a-f]+\((feat|fix|refactor|docs|chore|test|ci)\([^)]+\):' | head -1
if [[ $(echo "$COMMIT" | grep -oE '(ISSUE-|#)[0-9]+') ]]; then
echo "✅ Valid issue ID format"
set -u
if [ -n "${BASE:-}" ] && [ -n "${HEAD:-}" ]; then
RANGE="$BASE..$HEAD"
else
echo "❌ Missing issue ID (format: feat(scope): description (#123) or fix(scope): description (ISSUE-123))"
exit 1
RANGE="-1"
fi

session-check:
name: Verify Dev Session State
runs-on: ubuntu-latest
steps:
- name: Check session state before commit
run: |
if [ ! -f .trinity/dev_session.json ]; then
echo "❌ No dev session found"
exit 1
# Merge subjects are generated by GitHub, not by an author, so
# holding them to the format would fail every merge.
SUBJECTS=$(git log --no-merges --format=%s $RANGE)
if [ -z "$SUBJECTS" ]; then
echo "no non-merge commits to check"
exit 0
fi

STATE=$(jq -r '.state' .trinity/dev_session.json 2>/dev/null || echo "IDLE")
ISSUE=$(jq -r '.issue_number' .trinity/dev_session.json 2>/dev/null || echo "0")
PATTERN='^(feat|fix|refactor|docs|chore|test|ci|perf|build|style)(\([a-z0-9._/-]+\))?!?: .+'
ok=0; bad=0
while IFS= read -r s; do
if echo "$s" | grep -qE "$PATTERN"; then
ok=$((ok + 1))
else
echo " off-format: $s"
bad=$((bad + 1))
fi
done <<< "$SUBJECTS"

if [ "$STATE" != "TESTED" ] && [ "$STATE" != "COMMITTED" ]; then
echo "❌ Invalid state: $STATE (must be TESTED or COMMITTED)"
echo " Run: tri dev test to pass tests"
exit 1
echo "$ok in CLAUDE.md format, $bad not"
if [ "$bad" -gt 0 ]; then
echo "::warning::$bad of $((ok + bad)) subject(s) are not <type>(<scope>): <description>"
fi

if [ "$ISSUE" = "0" ]; then
echo "❌ No active issue"
exit 1
- name: Issue reference
if: github.event_name == 'pull_request'
env:
BASE: ${{ github.event.pull_request.base.sha }}
HEAD: ${{ github.event.pull_request.head.sha }}
BODY: ${{ github.event.pull_request.body }}
TITLE: ${{ github.event.pull_request.title }}
run: |
set -u
BLOB="$(git log --no-merges --format='%s %b' "$BASE..$HEAD")
$TITLE
$BODY"
if echo "$BLOB" | grep -qoE '(ISSUE-|#)[0-9]+'; then
echo "issue reference found: $(echo "$BLOB" | grep -oE '(ISSUE-|#)[0-9]+' | sort -u | tr '\n' ' ')"
else
echo "::warning::no issue reference (#123 or ISSUE-123) in the commits, title or body"
fi

echo "✅ Session state valid: $STATE for issue #$ISSUE"

pr-status:
name: Update PR Status
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Add in-progress label
run: |
gh pr edit "$PR_NUMBER" --add-label "status:in-progress"
- name: Update to done when merged
if: github.event.action == 'closed' && github.event.pull_request.merged == true
run: |
gh pr edit "$PR_NUMBER" --remove-label "status:in-progress"
gh pr edit "$PR_NUMBER" --add-label "status:completed"
Loading