yadflow 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +21 -0
  3. package/README.md +559 -0
  4. package/bin/sdlc.mjs +135 -0
  5. package/cli/commit.mjs +81 -0
  6. package/cli/epic-state.mjs +220 -0
  7. package/cli/gate.mjs +456 -0
  8. package/cli/lib.mjs +142 -0
  9. package/cli/manifest.mjs +119 -0
  10. package/cli/openpr.mjs +65 -0
  11. package/cli/plan.mjs +127 -0
  12. package/cli/platform.mjs +151 -0
  13. package/cli/reconcile.mjs +83 -0
  14. package/cli/repo.mjs +61 -0
  15. package/cli/setup.mjs +208 -0
  16. package/package.json +51 -0
  17. package/skills/sdlc/config.yaml +156 -0
  18. package/skills/sdlc/install.sh +51 -0
  19. package/skills/sdlc/module-help.csv +17 -0
  20. package/skills/sdlc-author-analysis/SKILL.md +136 -0
  21. package/skills/sdlc-author-architecture/SKILL.md +180 -0
  22. package/skills/sdlc-author-architecture/references/contract-format.md +72 -0
  23. package/skills/sdlc-author-epic/SKILL.md +154 -0
  24. package/skills/sdlc-author-epic/references/state-schema.md +187 -0
  25. package/skills/sdlc-author-stories/SKILL.md +109 -0
  26. package/skills/sdlc-author-stories/references/story-schema.md +46 -0
  27. package/skills/sdlc-author-ui/SKILL.md +113 -0
  28. package/skills/sdlc-backfill/SKILL.md +91 -0
  29. package/skills/sdlc-backfill/references/backfill.md +66 -0
  30. package/skills/sdlc-backfill/templates/checks/backfill-check.sh +42 -0
  31. package/skills/sdlc-checks/SKILL.md +138 -0
  32. package/skills/sdlc-checks/references/check-gates.md +168 -0
  33. package/skills/sdlc-checks/templates/checks/build-test-lint.sh +14 -0
  34. package/skills/sdlc-checks/templates/checks/contract-check.sh +62 -0
  35. package/skills/sdlc-checks/templates/checks/spec-link.sh +38 -0
  36. package/skills/sdlc-checks/templates/checks/verified-commits.sh +120 -0
  37. package/skills/sdlc-checks/templates/github/sdlc-checks.yml +45 -0
  38. package/skills/sdlc-checks/templates/github/sdlc-verified-commits.yml +22 -0
  39. package/skills/sdlc-checks/templates/gitlab/.gitlab-ci.yml +40 -0
  40. package/skills/sdlc-checks/templates/gitlab/gitlab-ci.include-root.yml +7 -0
  41. package/skills/sdlc-checks/templates/gitlab/sdlc-checks.gitlab-ci.yml +47 -0
  42. package/skills/sdlc-checks/templates/gitlab/sdlc-verified-commits.gitlab-ci.yml +21 -0
  43. package/skills/sdlc-connect-repos/SKILL.md +159 -0
  44. package/skills/sdlc-connect-repos/references/code-context.md +92 -0
  45. package/skills/sdlc-connect-repos/references/hub-config.md +77 -0
  46. package/skills/sdlc-connect-repos/references/repos-registry.md +62 -0
  47. package/skills/sdlc-hub-bridge/SKILL.md +119 -0
  48. package/skills/sdlc-hub-bridge/references/bridge.md +136 -0
  49. package/skills/sdlc-hub-bridge/references/login-roster.md +42 -0
  50. package/skills/sdlc-hub-bridge/templates/checks/hub-route.sh +50 -0
  51. package/skills/sdlc-hub-bridge/templates/github/sdlc-gate-sync.yml +63 -0
  52. package/skills/sdlc-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +7 -0
  53. package/skills/sdlc-hub-bridge/templates/gitlab/sdlc-gate-sync.gitlab-ci.yml +64 -0
  54. package/skills/sdlc-implement/SKILL.md +143 -0
  55. package/skills/sdlc-implement/references/implement-conventions.md +103 -0
  56. package/skills/sdlc-implement/templates/.gitmessage +17 -0
  57. package/skills/sdlc-pr-template/SKILL.md +86 -0
  58. package/skills/sdlc-pr-template/references/risk-routing.md +54 -0
  59. package/skills/sdlc-pr-template/templates/checks/risk-route.sh +44 -0
  60. package/skills/sdlc-pr-template/templates/github/pull_request_template.md +30 -0
  61. package/skills/sdlc-pr-template/templates/gitlab/merge_request_templates/Default.md +32 -0
  62. package/skills/sdlc-pr-template/templates/hub/github/pull_request_template.md +36 -0
  63. package/skills/sdlc-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +37 -0
  64. package/skills/sdlc-review-comments/SKILL.md +63 -0
  65. package/skills/sdlc-review-comments/references/comment-conventions.md +55 -0
  66. package/skills/sdlc-review-comments/templates/github/REVIEW_COMMENTS.md +49 -0
  67. package/skills/sdlc-review-comments/templates/gitlab/REVIEW_COMMENTS.md +49 -0
  68. package/skills/sdlc-review-gate/SKILL.md +196 -0
  69. package/skills/sdlc-review-gate/references/gating.md +79 -0
  70. package/skills/sdlc-run/SKILL.md +109 -0
  71. package/skills/sdlc-run/references/run-loop.md +121 -0
  72. package/skills/sdlc-ship/SKILL.md +86 -0
  73. package/skills/sdlc-ship/references/ship-and-record.md +67 -0
  74. package/skills/sdlc-ship/templates/.coderabbit.yaml +19 -0
  75. package/skills/sdlc-spec/SKILL.md +119 -0
  76. package/skills/sdlc-spec/references/spec-handoff.md +101 -0
  77. package/skills/sdlc-status/SKILL.md +92 -0
@@ -0,0 +1,168 @@
1
+ # Check gates — definitions, scripts, CI wiring, convention map
2
+
3
+ The gates are the production-safety core of the build half (Phase 3 build plan §C). They are
4
+ deliberately small, separate, and CI-agnostic: plain bash in `checks/`, invoked by whatever CI the
5
+ repo uses. Each reads conventions established by earlier steps — it invents nothing.
6
+
7
+ ## What each gate reads (the convention map)
8
+
9
+ | Gate | Reads | Source step |
10
+ |------|-------|-------------|
11
+ | spec-link | the `Task: <story>-<task>` commit trailer; `specs/<story>/link.md` | `sdlc-implement` (trailer), `sdlc-spec` (link.md) |
12
+ | contract-check | changed files under `specs/<story>/contracts/`; the `Contract-Change: yes` trailer; `link.md`'s pinned `contract-lock`; the product repo's `contract-lock.json` | `sdlc-author-architecture` (lock), `sdlc-spec` (slice + link), `sdlc-implement` (trailer) |
13
+ | build/test/lint | the repo's `npm run lint` / `npm run build` / `npm test` | the repo |
14
+ | verified-commits | each commit's platform signature-verification status; the author email vs `.sdlc/verified-authors` | hub roster `email` fields (`sdlc check --fix` generates the allowlist) |
15
+
16
+ ## 1. spec-link (`templates/checks/spec-link.sh`)
17
+
18
+ - Collects the `Task:` trailers across `<base>..HEAD`.
19
+ - **FAIL** if there is no `Task:` trailer (the change does not link a story/spec).
20
+ - For each `Task: <story>-<task>`, strips the `-T<NN>` suffix to get `<story>` and requires
21
+ `specs/<story>/link.md` to exist. **FAIL** if missing.
22
+ - Portable across bash 3.2 (macOS) and 4+ (no `mapfile`).
23
+ - **Fails closed** when `<base>` can't be resolved (so a shallow clone / wrong base never PASSes blind).
24
+
25
+ ## 2. contract-check (`templates/checks/contract-check.sh`)
26
+
27
+ - **Fails closed** if `<base>` can't be resolved — an undiffable range must never report "no surface
28
+ change" and silently green-light a bypass.
29
+ - Computes the changed files in `<base>..HEAD`.
30
+ - If **nothing** under `specs/*/contracts/**` changed → **PASS** (normal implementation only *consumes*
31
+ the contract).
32
+ - If the surface slice changed:
33
+ - Require a `Contract-Change: yes` trailer. **FAIL** (route back to the architecture gate) if absent.
34
+ - Best-effort fidelity: when the product repo is reachable (via `link.md`'s `product-repo` path),
35
+ require `link.md`'s pinned `contract-lock` hash to match the product repo's current
36
+ `contract-lock.json`. A claimed change that still pins the **old** lock **FAILS** — re-run
37
+ `sdlc-spec` so the slice matches the re-locked contract.
38
+ - This enforces the Phase 2 rule: the shared surface is owned upstream and is never widened from inside
39
+ a code repo. The hash recipe is in `../sdlc-author-architecture/references/contract-format.md`.
40
+
41
+ ## 3. build/test/lint (`templates/checks/build-test-lint.sh`)
42
+
43
+ - Runs `npm run lint`, `npm run build`, `npm test` in order; any non-zero exit fails the gate.
44
+ - Tests must actually exercise behavior (build plan §C) — an empty or trivially-passing suite does not
45
+ satisfy the gate's intent.
46
+
47
+ ### Canonical `package.json` scripts (Node demo)
48
+
49
+ ```json
50
+ {
51
+ "scripts": {
52
+ "lint": "find src -name '*.js' -print0 | xargs -0 -n1 node --check",
53
+ "build": "true",
54
+ "test": "node --test"
55
+ }
56
+ }
57
+ ```
58
+
59
+ `node --check` is a real syntax lint with no extra dependency; `node --test` is Node 20+'s built-in
60
+ runner. Real repos substitute their own eslint/tsc/jest — the gate only calls the scripts.
61
+
62
+ ## 4. verified-commits (`templates/checks/verified-commits.sh`)
63
+
64
+ No unverified commits from unverified users reach merge — on the product hub and on every connected
65
+ repo. For each commit in `<base>..HEAD`, two independent checks:
66
+
67
+ - **Verified signature** — the platform must mark the commit's signature verified (the GitHub/GitLab
68
+ "Verified" badge: signed with a GPG/SSH key registered to the account owning the author email).
69
+ Read via `gh api repos/{owner}/{repo}/commits/<sha>` (GitHub) or the commits/signature API (GitLab).
70
+ - **Known author** — the commit's **author email** must appear in `.sdlc/verified-authors`, generated
71
+ by `sdlc check --fix` from the hub roster's `email`/`emails` fields plus hub.json's
72
+ `verified_authors` list (edit hub.json, never the generated file). Only the author is checked:
73
+ platform-generated merge/squash commits set the platform as committer, and their integrity is
74
+ covered by the signature check.
75
+
76
+ Degradation is explicit, never silent: a missing allowlist SKIPs the author check with a warning
77
+ (configure roster emails, re-wire); no GitHub/GitLab remote SKIPs the signature check (the badge is a
78
+ platform concept — this keeps local runs and tests meaningful); an unreachable platform API **fails
79
+ closed** with guidance. GitLab CI needs a `GITLAB_TOKEN`/`SDLC_API_TOKEN` variable with `read_api` —
80
+ `CI_JOB_TOKEN` cannot read the signature API.
81
+
82
+ Note the deliberate split with the gate-sync bot: this gate runs on **PRs/MRs only**, so the
83
+ `sdlc-gate-sync` ledger commits (pushed directly to the default branch, unsigned, bot-authored) are
84
+ not subject to it. Do **not** replace it with a platform-level "reject unsigned pushes" rule on the
85
+ default branch — that would break the event-driven gate sync (and GitLab push rules are Premium-only).
86
+
87
+ ## CI wiring (both platforms)
88
+
89
+ The gates run identically under either CI; the config just invokes the scripts with the PR/MR base.
90
+
91
+ - **GitHub Actions** — `templates/github/sdlc-checks.yml` → `.github/workflows/sdlc-checks.yml`. The
92
+ jobs run on `pull_request` with `fetch-depth: 0`, passing `origin/${{ github.base_ref }}` as base
93
+ (verified-commits also gets a read-only `GH_TOKEN` for the Verified-badge lookup).
94
+ - **GitLab CI** — `templates/gitlab/sdlc-checks.gitlab-ci.yml` → `.gitlab/ci/sdlc-checks.yml`, pulled in
95
+ by the root `.gitlab-ci.yml`'s `include:`. The jobs run on `merge_request_event` with `GIT_DEPTH: 0`,
96
+ passing `origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME`.
97
+
98
+ ## Sync with existing CI (merge, never clobber)
99
+
100
+ `wire` is **additive**: it brings the SDLC gates into a repo that may already have CI, without ever
101
+ editing a foreign CI file. The principle is "own a separate file; touch the foreign root only to add a
102
+ one-line include".
103
+
104
+ **GitHub.** Every workflow file runs independently, so the gates simply live in their own
105
+ `sdlc-checks.yml`, identified by the first-line marker `# sdlc-managed: sdlc-checks`.
106
+ - No file at our path → copy the template verbatim.
107
+ - Our marked file already there → refresh it (no-op if identical).
108
+ - A **foreign** workflow occupies the name → install as `sdlc-checks.gen.yml` instead and make the
109
+ display `name:` unique. We never merge jobs into, or edit, a foreign workflow.
110
+
111
+ **GitLab.** Only one root `.gitlab-ci.yml` may exist, so the gates live in an **includable** fragment
112
+ `.gitlab/ci/sdlc-checks.yml` (marker `# sdlc-managed-include: sdlc-checks`). Its jobs declare `needs: []`
113
+ and **no `stage:`**, so they run in the default stage and a foreign root's `stages:` list can neither
114
+ break nor reorder them; job names are `sdlc-`prefixed to avoid collisions.
115
+ - No root `.gitlab-ci.yml` → write a minimal root (`gitlab-ci.include-root.yml`) that only `include:`s
116
+ the fragment.
117
+ - Root exists → read its top-level `include:`; add the key if absent, append
118
+ `- local: '.gitlab/ci/sdlc-checks.yml'` if missing, no-op if already present. **Nothing else** in the
119
+ root is touched.
120
+ - Root YAML cannot be parsed safely → **STOP** and print the include snippet for the human to paste.
121
+
122
+ **package.json.** Only ADD a missing `lint`/`build`/`test` script; an existing one is never overwritten.
123
+
124
+ **Idempotent.** The two markers plus the include-entry check make a re-run a no-op. This is how a repo
125
+ that already had its own pipeline keeps it and still gains the gates.
126
+
127
+ ## Wiring the hub (`repo: hub`)
128
+
129
+ The product hub is itself a repo on a platform (recorded in `.sdlc/hub.json` by
130
+ `sdlc-connect-repos action: detect-hub`). `wire repo: hub` targets `{project-root}` and uses the same
131
+ merge-not-clobber logic, with a **hub-flavored gate set** appropriate to a "thinking" repo (it has no
132
+ `specs/` or `package.json` build):
133
+ - **owner-set** — every `epic.md` (and forward artifact) under `epics/EP-*/` carries an `owner`.
134
+ - **contract-locked** — where an epic has a `contract.md`, its surface hash matches
135
+ `.sdlc/contract-lock.json` (reuse the recipe in
136
+ `../sdlc-author-architecture/references/contract-format.md`).
137
+ - **approvals-present** — an epic at `ready-for-build` has the approvals its gate rule requires recorded
138
+ in `.sdlc/approvals.json` (the same predicate `sdlc-review-gate` enforces).
139
+
140
+ These are advisory checks on the hub's own PRs (the front-half review PRs the bridge opens); they keep
141
+ the hub's artifacts internally consistent. The hub never runs the code-repo `spec-link`/`build-test-lint`
142
+ gates. Author the hub gate scripts under the hub's `checks/` following the same CI-agnostic-bash pattern.
143
+
144
+ The hub **does** run the verified-commits gate — `sdlc check --fix` installs `checks/verified-commits.sh`
145
+ plus a standalone workflow (`templates/github/sdlc-verified-commits.yml` →
146
+ `.github/workflows/sdlc-verified-commits.yml`, or the GitLab fragment
147
+ `templates/gitlab/sdlc-verified-commits.gitlab-ci.yml` → `.gitlab/ci/sdlc-verified-commits.yml` +
148
+ its one include line) whenever `.sdlc/hub.json` has a platform with the bridge enabled. So the
149
+ front-half review PRs are held to the same rule as code-repo PRs: signed, known authors only.
150
+
151
+ ## Running by hand (Phase 3 is manual)
152
+
153
+ From inside the code repo, against the PR/MR base (e.g. `master`):
154
+
155
+ ```bash
156
+ bash checks/spec-link.sh master
157
+ bash checks/contract-check.sh master
158
+ bash checks/build-test-lint.sh
159
+ bash checks/verified-commits.sh master # uses your own gh/glab auth for the signature lookup
160
+ ```
161
+
162
+ ## Proven behavior (demo: `demo-repos/backend`, story EP-istifta-inquiries-S01)
163
+
164
+ - **Good PR** (task branch with a `Task:` trailer, no surface change, passing tests) → all three **PASS**.
165
+ - **Bad PR A** (a code change committed with **no** `Task:` trailer) → spec-link **FAILS**.
166
+ - **Bad PR B** (edits `specs/.../contracts/inquiries.md` to widen the surface, with a `Task:` trailer
167
+ but **no** `Contract-Change`) → spec-link passes, contract-check **FAILS** and routes back to the
168
+ architecture gate.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ # build / test / lint gate (Phase 3 build plan §C).
3
+ # Standard quality stage: lint, build, and tests that actually exercise behavior (not just pass).
4
+ # Delegates to the repo's npm scripts so each repo owns the specifics.
5
+ set -euo pipefail
6
+
7
+ echo "[build/test/lint] lint…"
8
+ npm run --silent lint
9
+ echo "[build/test/lint] build…"
10
+ npm run --silent build
11
+ echo "[build/test/lint] test…"
12
+ npm run --silent test
13
+
14
+ echo "PASS [build/test/lint]: lint, build, and tests all green."
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bash
2
+ # contract-check gate (Phase 3 build plan §C; contract representation from Phase 2).
3
+ # The contract surface is singular and owned upstream (the product repo's locked contract.md).
4
+ # A code repo carries its quoted slice under specs/<story>/contracts/. If the diff changes that
5
+ # slice (i.e. tries to move the shared surface from inside a code repo), it MUST carry a
6
+ # `Contract-Change: yes` trailer AND the contract must have been updated/re-locked upstream first
7
+ # (link.md's pinned hash must match the product lock). Otherwise FAIL and route back to the
8
+ # architecture gate. Normal implementation that only CONSUMES the contract passes untouched.
9
+ set -euo pipefail
10
+
11
+ BASE="${1:-${SDLC_BASE:-origin/main}}"
12
+
13
+ # Fail CLOSED if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
14
+ # Never let an undiffable range silently report "no surface change" — that would green-light a bypass.
15
+ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
16
+ echo "FAIL [contract-check]: base ref '${BASE}' not found — fetch full history / check the base branch."
17
+ exit 1
18
+ fi
19
+ RANGE="${BASE}..HEAD"
20
+
21
+ changed="$(git diff --name-only "$RANGE")"
22
+ surface="$(printf '%s\n' "$changed" | grep -E '^specs/[^/]+/contracts/' || true)"
23
+
24
+ if [ -z "$surface" ]; then
25
+ echo "PASS [contract-check]: diff does not touch the contract surface (specs/*/contracts/**)."
26
+ exit 0
27
+ fi
28
+
29
+ echo "note [contract-check]: diff touches the contract surface:"
30
+ printf '%s\n' "$surface" | sed 's/^/ /'
31
+
32
+ cc="$(git log "$RANGE" --format='%(trailers:key=Contract-Change,valueonly)' | sed '/^$/d' | tr 'A-Z' 'a-z')"
33
+ if ! printf '%s\n' "$cc" | grep -qx 'yes'; then
34
+ echo "FAIL [contract-check]: contract surface changed without a 'Contract-Change: yes' trailer."
35
+ echo " -> Route back to the architecture gate: update + re-lock contract.md in the product repo,"
36
+ echo " re-run sdlc-spec, then implement with Contract-Change: yes. The surface is never widened"
37
+ echo " from inside a code repo."
38
+ exit 1
39
+ fi
40
+
41
+ # Fidelity check (best-effort): when the product repo is reachable, the story's link.md must pin the
42
+ # CURRENT product lock — proof the contract was actually updated/re-locked upstream, not just flagged.
43
+ story="$(printf '%s\n' "$surface" | head -1 | sed -E 's#^specs/([^/]+)/contracts/.*#\1#')"
44
+ link="specs/${story}/link.md"
45
+ if [ -f "$link" ]; then
46
+ product_rel="$(sed -nE 's/^product-repo:[[:space:]]*(.*)$/\1/p' "$link" | head -1)"
47
+ pinned="$(sed -nE 's/^contract-lock:[[:space:]]*sha256:([0-9a-f]+).*$/\1/p' "$link" | head -1)"
48
+ epic="$(printf '%s' "$story" | sed -E 's/-S[0-9]+$//')" # story EP-<slug>-S0N -> epic EP-<slug>
49
+ lock="${product_rel}/epics/${epic}/.sdlc/contract-lock.json"
50
+ if [ -n "$product_rel" ] && [ -f "$lock" ]; then
51
+ current="$(sed -nE 's/.*"hash":[[:space:]]*"sha256:([0-9a-f]+)".*/\1/p' "$lock" | head -1)"
52
+ if [ -n "$current" ] && [ "$current" != "$pinned" ]; then
53
+ echo "FAIL [contract-check]: Contract-Change claimed, but ${link} still pins ${pinned:0:12}…"
54
+ echo " while the product lock is ${current:0:12}… — re-run sdlc-spec so the slice matches the re-locked contract."
55
+ exit 1
56
+ fi
57
+ echo "note [contract-check]: link.md hash matches the product lock (${current:0:12}…)."
58
+ fi
59
+ fi
60
+
61
+ echo "PASS [contract-check]: surface change accompanied by Contract-Change: yes (and an updated contract)."
62
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # spec-link gate (Phase 3 build plan §C).
3
+ # The change must link a real story/spec: every commit range under review must carry a
4
+ # `Task: <story>-<task>` trailer whose <story> resolves to a specs/<story>/link.md.
5
+ # Fail if the link is missing — no unlinked code reaches merge.
6
+ set -euo pipefail
7
+
8
+ BASE="${1:-${SDLC_BASE:-origin/main}}"
9
+
10
+ # Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
11
+ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
12
+ echo "FAIL [spec-link]: base ref '${BASE}' not found — fetch full history / check the base branch."
13
+ exit 1
14
+ fi
15
+ RANGE="${BASE}..HEAD"
16
+
17
+ # Portable across bash 3.2 (macOS) and 4+ — no mapfile.
18
+ tasks="$(git log "$RANGE" --format='%(trailers:key=Task,valueonly)' | sed '/^$/d' | sort -u)"
19
+
20
+ if [ -z "$tasks" ]; then
21
+ echo "FAIL [spec-link]: no 'Task: <story>-<task>' trailer in ${RANGE} — change does not link a story/spec."
22
+ exit 1
23
+ fi
24
+
25
+ rc=0
26
+ while IFS= read -r t; do
27
+ [ -z "$t" ] && continue
28
+ story="$(printf '%s' "$t" | sed -E 's/-T[0-9]+$//')"
29
+ if [ -f "specs/${story}/link.md" ]; then
30
+ echo "PASS [spec-link]: ${t} -> specs/${story}/link.md"
31
+ else
32
+ echo "FAIL [spec-link]: ${t} references specs/${story}/ but link.md is missing."
33
+ rc=1
34
+ fi
35
+ done <<EOF
36
+ $tasks
37
+ EOF
38
+ exit "$rc"
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env bash
2
+ # verified-commits gate.
3
+ # Every commit in the range under review must (1) carry a signature the PLATFORM marks as verified
4
+ # (the GitHub/GitLab "Verified" badge — proves the commit was signed by a key registered to the
5
+ # account that owns the author email) and (2) be AUTHORED by a known identity: the author email must
6
+ # appear in the .sdlc/verified-authors allowlist (generated by `sdlc check --fix` from the hub
7
+ # roster's `email` fields + hub.json `verified_authors`). Unsigned commits and commits from unknown
8
+ # authors do not reach merge — on the product hub and on every connected repo alike.
9
+ #
10
+ # Only the AUTHOR email is checked against the allowlist: platform-generated merge/squash/update
11
+ # commits set the platform itself as the committer (e.g. noreply@github.com), and their integrity is
12
+ # covered by the signature check (the platform signs them).
13
+ #
14
+ # Degradation is explicit, never silent:
15
+ # - no allowlist file -> author check SKIPPED with a warning (configure emails, re-wire)
16
+ # - no GitHub/GitLab remote -> signature check SKIPPED with a warning (no platform, no badge)
17
+ # - platform API unreachable -> FAIL closed (a security gate must not pass on a broken check)
18
+ #
19
+ # GitLab note: the signature API is not readable with CI_JOB_TOKEN — provide a CI variable
20
+ # GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope; see the pipeline fragment header.
21
+ set -euo pipefail
22
+
23
+ BASE="${1:-${SDLC_BASE:-origin/main}}"
24
+
25
+ # Fail closed if the base ref can't be resolved (shallow clone / wrong base branch / unfetched ref).
26
+ if ! git rev-parse --verify --quiet "${BASE}^{commit}" >/dev/null; then
27
+ echo "FAIL [verified-commits]: base ref '${BASE}' not found — fetch full history / check the base branch."
28
+ exit 1
29
+ fi
30
+ RANGE="${BASE}..HEAD"
31
+
32
+ commits="$(git rev-list "$RANGE")"
33
+ if [ -z "$commits" ]; then
34
+ echo "PASS [verified-commits]: no commits in ${RANGE}"
35
+ exit 0
36
+ fi
37
+
38
+ # ---- author allowlist (one email per line; # comments; case-insensitive) -------------------------
39
+ ALLOWLIST="${SDLC_VERIFIED_AUTHORS:-.sdlc/verified-authors}"
40
+ authors_on=0
41
+ if [ -f "$ALLOWLIST" ]; then
42
+ authors_on=1
43
+ else
44
+ echo "WARN [verified-commits]: ${ALLOWLIST} not found — author allowlist NOT enforced. Add 'email' to the hub roster (or hub.json 'verified_authors'), then run \`sdlc check --fix\`."
45
+ fi
46
+
47
+ # ---- platform for the signature check (override with SDLC_PLATFORM=github|gitlab|none) -----------
48
+ remote="$(git remote get-url origin 2>/dev/null || true)"
49
+ platform=""
50
+ case "$remote" in
51
+ *github*) platform=github ;;
52
+ *gitlab*) platform=gitlab ;;
53
+ esac
54
+ platform="${SDLC_PLATFORM:-$platform}"
55
+ case "$platform" in
56
+ github|gitlab) ;;
57
+ ""|none)
58
+ platform=""
59
+ echo "WARN [verified-commits]: no GitHub/GitLab remote — signature verification SKIPPED (the Verified badge is a platform concept)."
60
+ ;;
61
+ *)
62
+ # Fail closed: an unknown override must never let signature checks silently pass.
63
+ echo "FAIL [verified-commits]: unknown platform '${platform}' (SDLC_PLATFORM must be github|gitlab|none)."
64
+ exit 1
65
+ ;;
66
+ esac
67
+
68
+ # 0 when the platform marks the commit's signature verified.
69
+ signature_verified() {
70
+ local sha v body
71
+ sha="$1"
72
+ case "$platform" in
73
+ github)
74
+ v="$(gh api "repos/{owner}/{repo}/commits/${sha}" --jq '.commit.verification.verified' 2>/dev/null || echo api-error)"
75
+ [ "$v" = "true" ]
76
+ ;;
77
+ gitlab)
78
+ # In CI use the documented API directly; locally fall back to the user's own glab.
79
+ if [ -n "${CI_API_V4_URL:-}" ] && [ -n "${CI_PROJECT_ID:-}" ]; then
80
+ body="$(curl -fsS --header "PRIVATE-TOKEN: ${GITLAB_TOKEN:-${SDLC_API_TOKEN:-}}" \
81
+ "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/repository/commits/${sha}/signature" 2>/dev/null || true)"
82
+ else
83
+ body="$(glab api "projects/:id/repository/commits/${sha}/signature" 2>/dev/null || true)"
84
+ fi
85
+ printf '%s' "$body" | grep -qE '"verification_status"[[:space:]]*:[[:space:]]*"verified"'
86
+ ;;
87
+ *) return 1 ;; # unreachable (platform validated above) — keep the gate fail-closed regardless
88
+ esac
89
+ }
90
+
91
+ rc=0
92
+ while IFS= read -r sha; do
93
+ [ -z "$sha" ] && continue
94
+ short="$(git log -1 --format=%h "$sha")"
95
+ author="$(git log -1 --format=%ae "$sha" | tr '[:upper:]' '[:lower:]')"
96
+
97
+ if [ "$authors_on" = 1 ]; then
98
+ # tolerate CRLF / stray surrounding whitespace in a hand-edited allowlist
99
+ if grep -vE '^[[:space:]]*(#|$)' "$ALLOWLIST" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' \
100
+ | tr '[:upper:]' '[:lower:]' | grep -qxF "$author"; then
101
+ echo "PASS [verified-commits]: ${short} author <${author}> is a known identity"
102
+ else
103
+ echo "FAIL [verified-commits]: ${short} author <${author}> is not in ${ALLOWLIST} — unverified user."
104
+ rc=1
105
+ fi
106
+ fi
107
+
108
+ if [ -n "$platform" ]; then
109
+ if signature_verified "$sha"; then
110
+ echo "PASS [verified-commits]: ${short} signature verified by ${platform}"
111
+ else
112
+ echo "FAIL [verified-commits]: ${short} signature missing/unverified — sign commits (GPG/SSH key registered on ${platform}), or the signature API was unreachable (GitLab: set GITLAB_TOKEN/SDLC_API_TOKEN with read_api)."
113
+ rc=1
114
+ fi
115
+ fi
116
+ done <<EOF
117
+ $commits
118
+ EOF
119
+
120
+ exit "$rc"
@@ -0,0 +1,45 @@
1
+ # sdlc-managed: sdlc-checks
2
+ # SDLC check gates (Phase 3 build plan §C). The three gates run on every PR and must pass before
3
+ # merge. They are CI-agnostic bash in checks/ — this workflow just invokes them with the PR base.
4
+ # This workflow file is OWNED by sdlc-checks (the marker on line 1 identifies it). It runs
5
+ # independently of any other workflow in .github/workflows/, so wiring never edits a foreign workflow.
6
+ name: sdlc-checks
7
+ on:
8
+ pull_request:
9
+ branches: ["**"]
10
+
11
+ jobs:
12
+ spec-link:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ with: { fetch-depth: 0 }
17
+ - run: bash checks/spec-link.sh "origin/${{ github.base_ref }}"
18
+
19
+ contract-check:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ with: { fetch-depth: 0 }
24
+ - run: bash checks/contract-check.sh "origin/${{ github.base_ref }}"
25
+
26
+ build-test-lint:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ with: { fetch-depth: 0 }
31
+ - uses: actions/setup-node@v4
32
+ with: { node-version: "20" }
33
+ - run: bash checks/build-test-lint.sh
34
+
35
+ # No unverified commits from unverified users: platform-Verified signature + allowlisted author.
36
+ verified-commits:
37
+ runs-on: ubuntu-latest
38
+ permissions:
39
+ contents: read
40
+ env:
41
+ GH_TOKEN: ${{ github.token }} # read-only: gh api commits/<sha> for the Verified badge
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ with: { fetch-depth: 0 }
45
+ - run: bash checks/verified-commits.sh "origin/${{ github.base_ref }}"
@@ -0,0 +1,22 @@
1
+ # sdlc-managed: sdlc-checks
2
+ # verified-commits gate for the PRODUCT HUB: every PR (including the front-half review/EP-* PRs)
3
+ # must contain only commits whose signature the platform marks Verified AND whose author email is a
4
+ # known identity (.sdlc/verified-authors — generated by `sdlc check --fix` from the hub roster).
5
+ # Standalone workflow so it never collides with the hub-flavored sdlc-checks workflow.
6
+ name: sdlc-verified-commits
7
+ on:
8
+ pull_request:
9
+ branches: ["**"]
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ jobs:
15
+ verified-commits:
16
+ runs-on: ubuntu-latest
17
+ env:
18
+ GH_TOKEN: ${{ github.token }} # read-only: gh api commits/<sha> for the Verified badge
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ with: { fetch-depth: 0 }
22
+ - run: bash checks/verified-commits.sh "origin/${{ github.base_ref }}"
@@ -0,0 +1,40 @@
1
+ # SDLC check gates (Phase 3 build plan §C). The team's GitLab CI runs the same gates as
2
+ # pipeline stages on every merge request; they must pass before merge. The gates are CI-agnostic
3
+ # bash in checks/ — these stages just invoke them with the MR target as base.
4
+ stages: [spec-link, contract-check, build-test-lint, verified-commits]
5
+
6
+ default:
7
+ image: node:20
8
+
9
+ .mr_only:
10
+ rules:
11
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
12
+
13
+ variables:
14
+ GIT_DEPTH: "0" # full history so the gates can diff against the target branch
15
+
16
+ spec-link:
17
+ stage: spec-link
18
+ extends: .mr_only
19
+ script:
20
+ - bash checks/spec-link.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
21
+
22
+ contract-check:
23
+ stage: contract-check
24
+ extends: .mr_only
25
+ script:
26
+ - bash checks/contract-check.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
27
+
28
+ build-test-lint:
29
+ stage: build-test-lint
30
+ extends: .mr_only
31
+ script:
32
+ - bash checks/build-test-lint.sh
33
+
34
+ # Needs a CI/CD variable GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope — CI_JOB_TOKEN cannot
35
+ # read the commit-signature API.
36
+ verified-commits:
37
+ stage: verified-commits
38
+ extends: .mr_only
39
+ script:
40
+ - bash checks/verified-commits.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
@@ -0,0 +1,7 @@
1
+ # SDLC check gates — minimal root pipeline.
2
+ # Written by `sdlc-checks wire` ONLY when the repo has no existing root `.gitlab-ci.yml`.
3
+ # The gates themselves live in the included fragment so the same single source is reused whether
4
+ # or not a root pipeline already exists. If a root later grows real jobs, they coexist with the
5
+ # included sdlc-* jobs (which carry `needs: []` and no `stage:`).
6
+ include:
7
+ - local: '.gitlab/ci/sdlc-checks.yml'
@@ -0,0 +1,47 @@
1
+ # sdlc-managed-include: sdlc-checks
2
+ # SDLC check gates (Phase 3 build plan §C), as an INCLUDABLE fragment.
3
+ # This file is pulled into a repo's existing .gitlab-ci.yml via:
4
+ # include:
5
+ # - local: '.gitlab/ci/sdlc-checks.yml'
6
+ # so wiring NEVER edits the foreign root pipeline beyond adding that one include line.
7
+ #
8
+ # Merge-safety: these jobs do NOT declare a `stage:` and use `needs: []`, so they run in the
9
+ # pipeline's default stage regardless of any `stages:` order the foreign root defines — a foreign
10
+ # `stages:` list can neither break them nor reorder them. Job names are sdlc-prefixed to avoid
11
+ # colliding with the host project's own job names.
12
+ default:
13
+ image: node:20
14
+
15
+ .sdlc_mr_only:
16
+ rules:
17
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
18
+
19
+ variables:
20
+ GIT_DEPTH: "0" # full history so the gates can diff against the target branch
21
+
22
+ sdlc-spec-link:
23
+ extends: .sdlc_mr_only
24
+ needs: []
25
+ script:
26
+ - bash checks/spec-link.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
27
+
28
+ sdlc-contract-check:
29
+ extends: .sdlc_mr_only
30
+ needs: []
31
+ script:
32
+ - bash checks/contract-check.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
33
+
34
+ sdlc-build-test-lint:
35
+ extends: .sdlc_mr_only
36
+ needs: []
37
+ script:
38
+ - bash checks/build-test-lint.sh
39
+
40
+ # No unverified commits from unverified users: platform-Verified signature + allowlisted author.
41
+ # Needs a CI/CD variable GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope — CI_JOB_TOKEN cannot
42
+ # read the commit-signature API. Without it the job FAILS closed with guidance.
43
+ sdlc-verified-commits:
44
+ extends: .sdlc_mr_only
45
+ needs: []
46
+ script:
47
+ - bash checks/verified-commits.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
@@ -0,0 +1,21 @@
1
+ # sdlc-managed-include: sdlc-checks
2
+ # verified-commits gate for the PRODUCT HUB, as an INCLUDABLE fragment. Pulled into the hub's root
3
+ # .gitlab-ci.yml via:
4
+ # include:
5
+ # - local: '.gitlab/ci/sdlc-verified-commits.yml'
6
+ # Every MR (including the front-half review/EP-* MRs) must contain only commits whose signature the
7
+ # platform marks Verified AND whose author email is a known identity (.sdlc/verified-authors).
8
+ #
9
+ # Needs a CI/CD variable GITLAB_TOKEN (or SDLC_API_TOKEN) with read_api scope — CI_JOB_TOKEN cannot
10
+ # read the commit-signature API. The hub's SDLC_GATE_TOKEN (from the gate-sync wiring) also works:
11
+ # set GITLAB_TOKEN: $SDLC_GATE_TOKEN in the job if you prefer one token.
12
+ # Job name is sdlc-hub-prefixed so it can coexist with the code-repo fragment in one pipeline.
13
+ sdlc-hub-verified-commits:
14
+ needs: []
15
+ image: node:20
16
+ variables:
17
+ GIT_DEPTH: "0" # full history so the gate can diff against the target branch
18
+ rules:
19
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
20
+ script:
21
+ - bash checks/verified-commits.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"