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
6 changes: 3 additions & 3 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A reusable workflow cannot reliably check out *its own* repo's scripts: in a `wo

`scripts/*.sh` is the source of truth; the inline copy is a derived artifact. **Editing a script means updating its inline copy too**, or `check-inline-sync.sh` fails CI. The sync is byte-for-byte after known normalizations (10-space YAML indent strip, shebang strip, function-wrapper strip). The pairs are listed in `check-inline-sync.sh` (`INLINE_PAIRS`):

- `dependency-safety.yml` embeds `extract-deps.sh`, `check-release-age.sh`, `diff-touches-lockfile.sh`, `pr-body-to-deps.sh`, `classify-touched-paths.sh`, `pyproject-bump-extract.sh`, and `safety-verdict.sh`
- `dep-safety.yml` embeds `extract-deps.sh`, `check-release-age.sh`, `diff-touches-lockfile.sh`, `pr-body-to-deps.sh`, `classify-touched-paths.sh`, `pyproject-bump-extract.sh`, and `safety-verdict.sh`
- `tag-release.yml` embeds `bump-version-files.sh`

`lint-workflow-call.sh` is the partner guard: it fails CI if any `workflow_call` file reintroduces a caller-scoped ref as a checkout `ref:`.
Expand All @@ -39,13 +39,13 @@ A reusable workflow cannot reliably check out *its own* repo's scripts: in a `wo

**Consumer-facing reusable workflows:**

- `dependency-safety.yml` — verifies the native-Dependabot-cooldown invariant on each Dependabot PR. Pipeline: extract → fallback → guard → age check → GHSA/OSV scan → scorecard → comment → labels; the verdict layer is deterministic: `failure` on age violation (when `fail_on_age_violation: true`), `error` on extraction/scan failure, `success` otherwise. Verdict translation lives in `safety-verdict.sh`. No rescan companion — verifier is single-shot per PR event.
- `dep-safety.yml` — verifies the native-Dependabot-cooldown invariant on each Dependabot PR. Pipeline: extract → fallback → guard → age check → GHSA/OSV scan → scorecard → comment → labels; the verdict layer is deterministic: `failure` on age violation (when `fail_on_age_violation: true`), `error` on extraction/scan failure, `success` otherwise. Verdict translation lives in `safety-verdict.sh`. No rescan companion — verifier is single-shot per PR event. (Renamed from `dependency-safety.yml` on `main` to force a fresh workflow registry entry; public `@v3` consumers still pin to the historical filename until a new release is cut.)
- `tag-release.yml` — computes the next semver tag from Conventional Commits, optionally runs `bump-version-files.sh` against `.version-bump.json`, creates the tag via the GitHub Git Data API (so commits/tags auto-sign under the App identity). Requires a GitHub App key (`RELEASE_BOT_PRIVATE_KEY` secret, `RELEASE_BOT_APP_ID` var).
- `publish-pypi.yml` — `uv build` → TestPyPI (with install verification) → PyPI via OIDC trusted publishing → GitHub Release.

**Release machinery for this repo itself:** `release-self.yml` (manual `workflow_dispatch`) → calls `tag-release.yml` → calls `release.yml`. `release.yml` is `workflow_call`-only (no `push: tags` trigger) and floats the major/minor tags (`v2` → `v2.3` → `v2.3.1`). Merging to `main` does **not** auto-release; a release is always a deliberate `release-self.yml` dispatch.

**CI:** `ci-scripts.yml` (bats + inline-sync + workflow-call lint), `ci-safety.yml` (dogfoods `dependency-safety.yml` on this repo's own Dependabot PRs), `security.yml` (zizmor workflow analysis).
**CI:** `ci-scripts.yml` (bats + inline-sync + workflow-call lint), `ci-safety.yml` (dogfoods `dep-safety.yml` on this repo's own Dependabot PRs), `security.yml` (zizmor workflow analysis).

## Conventions

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-safety.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ concurrency:

jobs:
safety:
uses: ./.github/workflows/dependency-safety.yml
uses: ./.github/workflows/dep-safety.yml
with:
auto_merge: true
14 changes: 7 additions & 7 deletions scripts/check-inline-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ set -euo pipefail

# Each entry: "<workflow-yaml>:<script-path>"
INLINE_PAIRS=(
".github/workflows/dependency-safety.yml:scripts/extract-deps.sh"
".github/workflows/dependency-safety.yml:scripts/check-release-age.sh"
".github/workflows/dependency-safety.yml:scripts/diff-touches-lockfile.sh"
".github/workflows/dependency-safety.yml:scripts/pr-body-to-deps.sh"
".github/workflows/dependency-safety.yml:scripts/safety-verdict.sh"
".github/workflows/dependency-safety.yml:scripts/classify-touched-paths.sh"
".github/workflows/dep-safety.yml:scripts/extract-deps.sh"
".github/workflows/dep-safety.yml:scripts/check-release-age.sh"
".github/workflows/dep-safety.yml:scripts/diff-touches-lockfile.sh"
".github/workflows/dep-safety.yml:scripts/pr-body-to-deps.sh"
".github/workflows/dep-safety.yml:scripts/safety-verdict.sh"
".github/workflows/dep-safety.yml:scripts/classify-touched-paths.sh"
".github/workflows/tag-release.yml:scripts/bump-version-files.sh"
".github/workflows/dependency-safety.yml:scripts/pyproject-bump-extract.sh"
".github/workflows/dep-safety.yml:scripts/pyproject-bump-extract.sh"
)

YAML_INDENT=" " # exactly 10 spaces — matches the `run: |` indent
Expand Down
8 changes: 4 additions & 4 deletions tests/guard-runtime.bats
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ extract_guard_block() {
}

@test "safety: issue #62 case (DEPS_TSV non-empty + UNSUPPORTED non-empty) — guard fires" {
block=$(extract_guard_block .github/workflows/dependency-safety.yml)
block=$(extract_guard_block .github/workflows/dep-safety.yml)
DEPS_TSV=$'mypy\t1.20.1\tpypi'
TOUCHED_PATHS=$'package-lock.json\nuv.lock'
EFFECTIVE_TOUCHED="$TOUCHED_PATHS"
Expand All @@ -33,7 +33,7 @@ extract_guard_block() {
}

@test "safety: issue #52 case (DEPS_TSV empty + TOUCHED non-empty + UNSUPPORTED empty) — guard fires" {
block=$(extract_guard_block .github/workflows/dependency-safety.yml)
block=$(extract_guard_block .github/workflows/dep-safety.yml)
DEPS_TSV=""
TOUCHED_PATHS="uv.lock"
EFFECTIVE_TOUCHED="$TOUCHED_PATHS"
Expand All @@ -44,7 +44,7 @@ extract_guard_block() {
}

@test "safety: clean supported case (DEPS_TSV non-empty + UNSUPPORTED empty) — guard does NOT fire" {
block=$(extract_guard_block .github/workflows/dependency-safety.yml)
block=$(extract_guard_block .github/workflows/dep-safety.yml)
DEPS_TSV=$'requests\t2.32.0\tpypi'
TOUCHED_PATHS="requirements.txt"
EFFECTIVE_TOUCHED="$TOUCHED_PATHS"
Expand All @@ -54,7 +54,7 @@ extract_guard_block() {
}

@test "safety: no touched paths (empty diff) — guard does NOT fire" {
block=$(extract_guard_block .github/workflows/dependency-safety.yml)
block=$(extract_guard_block .github/workflows/dep-safety.yml)
DEPS_TSV=""
TOUCHED_PATHS=""
EFFECTIVE_TOUCHED=""
Expand Down
10 changes: 5 additions & 5 deletions tests/guard-shape.bats
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@
# unit-testable; this guard prevents accidental ordering regressions.

WORKFLOWS=(
".github/workflows/dependency-safety.yml"
".github/workflows/dep-safety.yml"
)

@test "dependency-safety.yml: TOUCHED_PATHS/UNSUPPORTED_PATHS hoisted above Layer 2" {
yaml=".github/workflows/dependency-safety.yml"
@test "dep-safety.yml: TOUCHED_PATHS/UNSUPPORTED_PATHS hoisted above Layer 2" {
yaml=".github/workflows/dep-safety.yml"
hoist_line=$(grep -n "UNSUPPORTED_PATHS=\$(printf" "$yaml" | head -1 | cut -d: -f1)
layer2_line=$(grep -n "Layer 2: PR-body fallback" "$yaml" | head -1 | cut -d: -f1)
[ -n "$hoist_line" ]
[ -n "$layer2_line" ]
[ "$hoist_line" -lt "$layer2_line" ]
}

@test "dependency-safety.yml: Layer 3 guard checks UNSUPPORTED_PATHS before zero-rows elif" {
yaml=".github/workflows/dependency-safety.yml"
@test "dep-safety.yml: Layer 3 guard checks UNSUPPORTED_PATHS before zero-rows elif" {
yaml=".github/workflows/dep-safety.yml"
unsupported_line=$(grep -n 'if \[ -n "\$UNSUPPORTED_PATHS" \]; then' "$yaml" | head -1 | cut -d: -f1)
elif_line=$(grep -n 'elif \[ -z "\$(echo "\$DEPS_TSV" | sed' "$yaml" | head -1 | cut -d: -f1)
[ -n "$unsupported_line" ]
Expand Down