Skip to content

feat(US0488, D0127): sprint next materialises the head charter agains… #545

feat(US0488, D0127): sprint next materialises the head charter agains…

feat(US0488, D0127): sprint next materialises the head charter agains… #545

Workflow file for this run

name: Lint
on:
push:
branches: [main]
pull_request:
branches: [main]
# The whole-corpus markdown lane is periodic and pre-release, never per-commit: the
# pre-commit gate already runs ~197s against a 120s budget, and a guard whose cost is
# paid on every commit gets switched off.
schedule:
- cron: '17 4 * * 1'
workflow_dispatch:
# Least privilege: the jobs only check out and run linters/tests - nothing writes to the
# repo, so the workflow token needs read only (BG0058). Without this the token defaults to
# the repository/org setting, which may be the legacy permissive token.
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Install Python deps (before any Python run)
# CR0207: `npm test` runs the config-driven tests, which fail (not skip) without PyYAML.
# Install it on the pinned interpreter BEFORE the first Python invocation, so the suite
# is not validated by accident of the runner image happening to preinstall PyYAML.
# BG0107: pytest is a genuine dependency of the bench harness's class-D grader
# (tools/bench/audit_quiz.py shells `python -m pytest` inside fixture workspaces).
run: python -m pip install --quiet pyyaml pytest
- name: Lint (markdown, style, links)
run: npm run lint
- name: Run script unit tests (tools/skill-tests.sh adds the green-run noise gate)
run: |
bash tools/skill-tests.sh
python3 -m unittest discover -s tools/tests
- name: Portable artefact gate (same gate the pre-commit hook runs)
run: |
# BG0096: the hook and CONTRIBUTING claimed CI re-runs this gate; now it does.
python3 .claude/skills/sdlc-studio/scripts/gate.py --root .
- name: Coverage gate (runtime scripts, >= 80%)
run: |
# PyYAML is required: the config-driven tests (provenance/validate adopt_after cutoff,
# transition done-gate, conformance) read .config.yaml via config._yaml() and FAIL (not
# skip) without it, which makes `coverage run` exit non-zero and fails this step before
# the threshold is ever checked. Coverage itself is healthy (~82%). See US0047.
python -m pip install --quiet coverage pyyaml
coverage run --source=.claude/skills/sdlc-studio/scripts -m unittest discover -s .claude/skills/sdlc-studio/scripts/tests
coverage report --omit='*/tests/*' --fail-under=80
- name: Python security scan (bandit)
run: |
python -m pip install --quiet bandit
bandit -r .claude/skills/sdlc-studio/scripts -ll -x '*/tests/*' -q
# The whole markdown corpus under the STRICT rule set - every tracked .md file,
# dot-directories included. The per-commit lanes cannot see inside `.claude/` with a
# `**/*.md` glob, so the payload is only ever linted under its own relaxed config; this
# job is where a rule that config switches off is still enforced. Findings are attributed
# against the latest tag, so the report says what the release introduced rather than
# reciting the whole backlog.
corpus:
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
# Full history and tags: the baseline is a tag, and attribution needs to
# materialise it. A shallow clone would report every finding unattributed.
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Markdown corpus lint (strict rules, attributed)
run: npm run lint:corpus
windows-smoke:
runs-on: windows-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- name: install.ps1 - list targets and dry run (offline)
shell: pwsh
run: |
& ./install.ps1 -ListTargets
& ./install.ps1 -Target all -DryRun
- name: install.ps1 - real local install with stale-copy sweep
shell: pwsh
run: |
# Pre-place a stale sdlc-studio copy where the sweep should find it.
$stale = Join-Path $HOME '.gemini/skills/sdlc-studio'
New-Item -ItemType Directory -Force (Join-Path $stale 'templates') | Out-Null
Set-Content -Path (Join-Path $stale 'SKILL.md') -Value "---`nname: sdlc-studio`n---`n"
Set-Content -Path (Join-Path $stale 'templates/version.yaml') -Value 'skill_version: "0.0.1"'
$proj = Join-Path $env:RUNNER_TEMP 'sdlc-smoke'
New-Item -ItemType Directory -Force $proj | Out-Null
Push-Location $proj
& "$env:GITHUB_WORKSPACE/install.ps1" -Target claude -Local
if (-not (Test-Path '.claude/skills/sdlc-studio/SKILL.md')) {
throw 'install.ps1 did not place SKILL.md'
}
# The sweep must have refreshed the stale gemini copy.
$swept = Get-Content (Join-Path $stale 'templates/version.yaml') -Raw
if ($swept -match '0\.0\.1') {
throw 'sweep did not refresh the stale gemini copy'
}
Write-Host 'Windows install + sweep refresh OK'
Pop-Location
- name: install.ps1 - NoSweep leaves other copies alone
shell: pwsh
run: |
# Reset the gemini copy to a stale version, install with -NoSweep,
# and assert it was left untouched.
$stale = Join-Path $HOME '.gemini/skills/sdlc-studio'
Set-Content -Path (Join-Path $stale 'templates/version.yaml') -Value 'skill_version: "0.0.1"'
$proj = Join-Path $env:RUNNER_TEMP 'sdlc-smoke-nosweep'
New-Item -ItemType Directory -Force $proj | Out-Null
Push-Location $proj
& "$env:GITHUB_WORKSPACE/install.ps1" -Target claude -Local -NoSweep
$kept = Get-Content (Join-Path $stale 'templates/version.yaml') -Raw
if ($kept -notmatch '0\.0\.1') {
throw 'NoSweep still modified the gemini copy'
}
Write-Host 'NoSweep opt-out OK'
Pop-Location