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.
- package/CHANGELOG.md +50 -0
- package/LICENSE +21 -0
- package/README.md +559 -0
- package/bin/sdlc.mjs +135 -0
- package/cli/commit.mjs +81 -0
- package/cli/epic-state.mjs +220 -0
- package/cli/gate.mjs +456 -0
- package/cli/lib.mjs +142 -0
- package/cli/manifest.mjs +119 -0
- package/cli/openpr.mjs +65 -0
- package/cli/plan.mjs +127 -0
- package/cli/platform.mjs +151 -0
- package/cli/reconcile.mjs +83 -0
- package/cli/repo.mjs +61 -0
- package/cli/setup.mjs +208 -0
- package/package.json +51 -0
- package/skills/sdlc/config.yaml +156 -0
- package/skills/sdlc/install.sh +51 -0
- package/skills/sdlc/module-help.csv +17 -0
- package/skills/sdlc-author-analysis/SKILL.md +136 -0
- package/skills/sdlc-author-architecture/SKILL.md +180 -0
- package/skills/sdlc-author-architecture/references/contract-format.md +72 -0
- package/skills/sdlc-author-epic/SKILL.md +154 -0
- package/skills/sdlc-author-epic/references/state-schema.md +187 -0
- package/skills/sdlc-author-stories/SKILL.md +109 -0
- package/skills/sdlc-author-stories/references/story-schema.md +46 -0
- package/skills/sdlc-author-ui/SKILL.md +113 -0
- package/skills/sdlc-backfill/SKILL.md +91 -0
- package/skills/sdlc-backfill/references/backfill.md +66 -0
- package/skills/sdlc-backfill/templates/checks/backfill-check.sh +42 -0
- package/skills/sdlc-checks/SKILL.md +138 -0
- package/skills/sdlc-checks/references/check-gates.md +168 -0
- package/skills/sdlc-checks/templates/checks/build-test-lint.sh +14 -0
- package/skills/sdlc-checks/templates/checks/contract-check.sh +62 -0
- package/skills/sdlc-checks/templates/checks/spec-link.sh +38 -0
- package/skills/sdlc-checks/templates/checks/verified-commits.sh +120 -0
- package/skills/sdlc-checks/templates/github/sdlc-checks.yml +45 -0
- package/skills/sdlc-checks/templates/github/sdlc-verified-commits.yml +22 -0
- package/skills/sdlc-checks/templates/gitlab/.gitlab-ci.yml +40 -0
- package/skills/sdlc-checks/templates/gitlab/gitlab-ci.include-root.yml +7 -0
- package/skills/sdlc-checks/templates/gitlab/sdlc-checks.gitlab-ci.yml +47 -0
- package/skills/sdlc-checks/templates/gitlab/sdlc-verified-commits.gitlab-ci.yml +21 -0
- package/skills/sdlc-connect-repos/SKILL.md +159 -0
- package/skills/sdlc-connect-repos/references/code-context.md +92 -0
- package/skills/sdlc-connect-repos/references/hub-config.md +77 -0
- package/skills/sdlc-connect-repos/references/repos-registry.md +62 -0
- package/skills/sdlc-hub-bridge/SKILL.md +119 -0
- package/skills/sdlc-hub-bridge/references/bridge.md +136 -0
- package/skills/sdlc-hub-bridge/references/login-roster.md +42 -0
- package/skills/sdlc-hub-bridge/templates/checks/hub-route.sh +50 -0
- package/skills/sdlc-hub-bridge/templates/github/sdlc-gate-sync.yml +63 -0
- package/skills/sdlc-hub-bridge/templates/gitlab/gitlab-ci.include-root.yml +7 -0
- package/skills/sdlc-hub-bridge/templates/gitlab/sdlc-gate-sync.gitlab-ci.yml +64 -0
- package/skills/sdlc-implement/SKILL.md +143 -0
- package/skills/sdlc-implement/references/implement-conventions.md +103 -0
- package/skills/sdlc-implement/templates/.gitmessage +17 -0
- package/skills/sdlc-pr-template/SKILL.md +86 -0
- package/skills/sdlc-pr-template/references/risk-routing.md +54 -0
- package/skills/sdlc-pr-template/templates/checks/risk-route.sh +44 -0
- package/skills/sdlc-pr-template/templates/github/pull_request_template.md +30 -0
- package/skills/sdlc-pr-template/templates/gitlab/merge_request_templates/Default.md +32 -0
- package/skills/sdlc-pr-template/templates/hub/github/pull_request_template.md +36 -0
- package/skills/sdlc-pr-template/templates/hub/gitlab/merge_request_templates/Default.md +37 -0
- package/skills/sdlc-review-comments/SKILL.md +63 -0
- package/skills/sdlc-review-comments/references/comment-conventions.md +55 -0
- package/skills/sdlc-review-comments/templates/github/REVIEW_COMMENTS.md +49 -0
- package/skills/sdlc-review-comments/templates/gitlab/REVIEW_COMMENTS.md +49 -0
- package/skills/sdlc-review-gate/SKILL.md +196 -0
- package/skills/sdlc-review-gate/references/gating.md +79 -0
- package/skills/sdlc-run/SKILL.md +109 -0
- package/skills/sdlc-run/references/run-loop.md +121 -0
- package/skills/sdlc-ship/SKILL.md +86 -0
- package/skills/sdlc-ship/references/ship-and-record.md +67 -0
- package/skills/sdlc-ship/templates/.coderabbit.yaml +19 -0
- package/skills/sdlc-spec/SKILL.md +119 -0
- package/skills/sdlc-spec/references/spec-handoff.md +101 -0
- 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"
|