yadflow 2.13.0 → 2.15.0
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 +2 -2
- package/README.md +28 -16
- package/bin/yad.mjs +55 -9
- package/cli/artifact-status.mjs +102 -0
- package/cli/doctor.mjs +35 -2
- package/cli/epic-state.mjs +86 -11
- package/cli/gate.mjs +130 -60
- package/cli/lib.mjs +3 -0
- package/cli/manifest.mjs +2 -0
- package/cli/next.mjs +123 -0
- package/cli/platform.mjs +44 -6
- package/cli/repo.mjs +8 -4
- package/cli/setup.mjs +215 -80
- package/package.json +1 -1
- package/skills/sdlc/config.yaml +8 -0
- package/skills/yad-analysis/SKILL.md +6 -3
- package/skills/yad-architecture/SKILL.md +8 -3
- package/skills/yad-checks/SKILL.md +7 -0
- package/skills/yad-checks/templates/checks/ledger-guard.sh +117 -0
- package/skills/yad-checks/templates/checks/verified-commits.sh +9 -1
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +8 -0
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +7 -0
- package/skills/yad-epic/SKILL.md +4 -1
- package/skills/yad-hub-bridge/SKILL.md +41 -14
- package/skills/yad-hub-bridge/references/bridge.md +93 -51
- package/skills/yad-hub-bridge/templates/github/yad-gate-sync.yml +85 -35
- package/skills/yad-hub-bridge/templates/gitlab/yad-gate-sync.gitlab-ci.yml +63 -32
- package/skills/yad-review-gate/SKILL.md +12 -10
- package/skills/yad-stories/SKILL.md +8 -3
- package/skills/yad-test-cases/SKILL.md +10 -5
- package/skills/yad-ui/SKILL.md +8 -3
|
@@ -34,3 +34,11 @@ jobs:
|
|
|
34
34
|
- run: |
|
|
35
35
|
body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
|
|
36
36
|
bash checks/pr-template.sh --profile hub "$body"
|
|
37
|
+
|
|
38
|
+
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
|
|
39
|
+
ledger-guard:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
with: { fetch-depth: 0 }
|
|
44
|
+
- run: bash checks/ledger-guard.sh "origin/${{ github.base_ref }}"
|
|
@@ -37,3 +37,10 @@ yad-hub-pr-template:
|
|
|
37
37
|
script:
|
|
38
38
|
- body="$(mktemp)"; printf '%s' "$CI_MERGE_REQUEST_DESCRIPTION" > "$body"
|
|
39
39
|
- bash checks/pr-template.sh --profile hub "$body"
|
|
40
|
+
|
|
41
|
+
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
|
|
42
|
+
yad-hub-ledger-guard:
|
|
43
|
+
extends: .yad_hub_mr_only
|
|
44
|
+
needs: []
|
|
45
|
+
script:
|
|
46
|
+
- bash checks/ledger-guard.sh "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
package/skills/yad-epic/SKILL.md
CHANGED
|
@@ -40,7 +40,10 @@ exists for the idea.
|
|
|
40
40
|
|
|
41
41
|
Either mode runs Step 3b (branch), Step 4 (write the epic), and Step 6 (stop at the gate).
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
**Precondition gate (rail):** if `state.json` already exists (analysis-ran or re-entry), run
|
|
44
|
+
`yad next EP-<slug> --check epic` — if it exits non-zero, **STOP** and surface the blocker, pointing the
|
|
45
|
+
user at `yad next EP-<slug>`. When no `state.json` exists yet, this is the greenfield entry point — the
|
|
46
|
+
check is not applicable, so proceed and seed state.
|
|
44
47
|
|
|
45
48
|
### Step 2 — Shape the idea (assist: analyst) — or read the analysis
|
|
46
49
|
- **Analysis skipped:** ask the user for a one-line feature idea if not provided, then adopt the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yad-hub-bridge
|
|
3
|
-
description: 'The templated PR/MR bridge for the front-half review gate. When the product hub has a platform (.sdlc/hub.json), it opens a review PR/MR on the hub for an authored artifact (the optional analysis / epic / architecture+contract / ui-design / stories / test-cases), sets the required reviewers/labels from the routing rule, and provides the read-only gh/glab recipes that yad-review-gate uses to pull platform comments + approvals back into the file ledger. Can also wire
|
|
3
|
+
description: 'The templated PR/MR bridge for the front-half review gate. When the product hub has a platform (.sdlc/hub.json), it opens a review PR/MR on the hub for an authored artifact (the optional analysis / epic / architecture+contract / ui-design / stories / test-cases), sets the required reviewers/labels from the routing rule, and provides the read-only gh/glab recipes that yad-review-gate uses to pull platform comments + approvals back into the file ledger. Can also wire merge-time sync on the hub: a CI workflow that runs `yad gate ci` when a human merges a review PR/MR — CI is the sole ledger writer and writes only at merge, on the default branch (during review the platform PR/MR is the source of truth; CI never touches the review branch). Local-user auth only — no stored tokens. The file ledger stays the source of truth; degrades to the file-only gate when there is no platform / no CLI. Use when the user says "open the review PR", "route the review", "wire the gate sync", or it is invoked by yad-review-gate open/sync.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SDLC — Hub Review Bridge (the templated PR/MR bridge)
|
|
@@ -57,24 +57,40 @@ each required domain-owner to a platform `login` via the roster (a roster `name`
|
|
|
57
57
|
filled with the epic, artifact, gate step, owner, `epic.repos`, and the step's risk tags.
|
|
58
58
|
3. **Request the required reviewers** (their logins) and add a `domain:<repo>` label per touched repo so
|
|
59
59
|
per-repo routing is legible on the PR.
|
|
60
|
-
4.
|
|
60
|
+
4. **Do not write the ledger.** CI is the sole writer and writes only at merge. During review nothing
|
|
61
|
+
is recorded in the ledger — the platform PR/MR holds the review state (native approvals + threads).
|
|
62
|
+
At merge, CI records the `hub-prs.json` entry (in the shape below) and advances on the default
|
|
63
|
+
branch:
|
|
61
64
|
```json
|
|
62
65
|
{ "step": "<review step id>", "artifact": "<artifact>", "platform": "github|gitlab",
|
|
63
66
|
"number": <n>, "url": "<pr/mr url>", "branch": "review/EP-<slug>/<artifact-base>", "lastSyncedAt": null }
|
|
64
67
|
```
|
|
68
|
+
A human commit touching the gate-state files (`.sdlc/{state,approvals,comments,hub-prs}.json` or
|
|
69
|
+
`reviews/*.md`; `.sdlc/contract-lock.json` is artifact-side and allowed) on a review PR is rejected
|
|
70
|
+
by the `ledger-guard` check. (The `yad gate open` CLI behaves the same: in bridge mode it opens the
|
|
71
|
+
PR only and writes no ledger.)
|
|
65
72
|
5. Report the PR/MR URL and the required reviewers. **Do not** record approvals or advance — reviewers
|
|
66
|
-
act on the platform; `yad
|
|
73
|
+
act on the platform; CI (`yad gate ci`) reconciles it onto the default branch at merge.
|
|
67
74
|
|
|
68
75
|
### Step 3 — `route` (print required reviewers)
|
|
69
76
|
Compute and print the required reviewers as above. Use `templates/checks/hub-route.sh <body>` to parse a
|
|
70
77
|
PR/MR body's Impact & Risk block when given one; otherwise derive from `epic.repos` + the step's
|
|
71
78
|
`risk_tags`. Advisory only — it routes the human review, it does not approve.
|
|
72
79
|
|
|
73
|
-
### Step 4 — `wire` (
|
|
74
|
-
Install the hub CI that turns
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
### Step 4 — `wire` (merge-time sync on the hub)
|
|
81
|
+
Install the hub CI that turns the human **merge** into a `yad gate ci` run, with CI as the **sole
|
|
82
|
+
writer** of the ledger. There is no pre-merge CI write — during review the platform PR/MR is the
|
|
83
|
+
source of truth (native approvals + threads). On merge, CI re-reads approvals from the platform,
|
|
84
|
+
advances the step, and flips the artifact `status:` on the **default branch** (the only place CI ever
|
|
85
|
+
commits). Also install the `ledger-guard` check (yad-checks) so humans cannot commit gate-state files.
|
|
86
|
+
Revoke-on-change is enforced at merge: on **GitHub** in code (an approval whose commit ≠ the merged
|
|
87
|
+
head is dropped — no setting needed); on **GitLab** it has no per-approval commit SHA, so enabling the
|
|
88
|
+
platform's **"remove all approvals when commits are added to the source branch"** is **required** for
|
|
89
|
+
the guarantee. Either way it is safe because CI never pushes the review branch — only the owner's own
|
|
90
|
+
artifact pushes dismiss approvals. In bridge mode `yad gate sync` is advisory (read-only) and is **not**
|
|
91
|
+
a recovery path; if a merge-time run fails, the scheduled reconcile job re-advances it automatically, or
|
|
92
|
+
a maintainer can force it with `yad gate ci --branch <review-branch> --pr <n> --merged` locally on the
|
|
93
|
+
default branch. (File-only mode keeps `yad gate sync` as the local writer.)
|
|
78
94
|
|
|
79
95
|
1. Run `yad check --fix` (the wiring is manifest-driven, like `yad-checks`): with a platform +
|
|
80
96
|
enabled bridge in `.sdlc/hub.json` it installs
|
|
@@ -87,8 +103,9 @@ remains valid and is the fallback whenever CI cannot push.
|
|
|
87
103
|
`templates/gitlab/gitlab-ci.include-root.yml` as the root when none exists;
|
|
88
104
|
- create the 15-minute pipeline **schedule** (variable `SDLC_GATE_SYNC=true`) and the masked
|
|
89
105
|
`SDLC_GATE_TOKEN` project-access-token variable (`read_api` + `write_repository`). GitLab fires no
|
|
90
|
-
pipeline on an approval alone,
|
|
91
|
-
|
|
106
|
+
pipeline on an approval alone, and a squash merge can drop the branch name, so the schedule is the
|
|
107
|
+
catch-up path that advances merged reviews it discovers via the API (≤ ~15 min latency); a merge
|
|
108
|
+
push whose commit names the review branch advances near-immediately.
|
|
92
109
|
- **If your runners are tag-locked** (`run_untagged: false`, common on self-hosted): set a
|
|
93
110
|
`YAD_RUNNER_TAGS` CI/CD variable (e.g. `dind_runner`) so the docker-image `yad-gate-sync` job
|
|
94
111
|
is routed to a runner — otherwise it sits `pending` forever. The fragment emits
|
|
@@ -98,8 +115,12 @@ remains valid and is the fallback whenever CI cannot push.
|
|
|
98
115
|
every `yad` sync. Single value only — `tags: [$VAR]` is one tag equal to the whole variable,
|
|
99
116
|
not a comma-split.
|
|
100
117
|
3. Commit the workflow to the hub. GitHub needs nothing else — the ephemeral `github.token` reads the
|
|
101
|
-
PR and pushes the
|
|
102
|
-
|
|
118
|
+
PR and pushes the merge advance to the default branch, and the workflow's reconcile **schedule**
|
|
119
|
+
runs automatically (no setup) as the safety net that recovers a merge whose run failed transiently.
|
|
120
|
+
If the default branch is protected, see the workflow header for the bypass / PAT options; until then
|
|
121
|
+
the run fails visibly — recover by granting the push path, then re-run the workflow or run
|
|
122
|
+
`yad gate ci --branch <review-branch> --pr <n> --merged` locally on the default branch (advisory
|
|
123
|
+
`yad gate sync` cannot recover a bridge gate).
|
|
103
124
|
|
|
104
125
|
## Hard rules
|
|
105
126
|
|
|
@@ -111,12 +132,18 @@ remains valid and is the fallback whenever CI cannot push.
|
|
|
111
132
|
act) — `yad gate sync` records the approvals + resolution + merged state and advances; unresolved
|
|
112
133
|
comments or a changed artifact hold it `in_review`. The mechanical sync is the `yad gate` CLI.
|
|
113
134
|
- **CI never approves and never merges.** The wired workflow only runs `gate ci` — the same sync +
|
|
114
|
-
unchanged predicate
|
|
115
|
-
|
|
135
|
+
unchanged predicate. It does **nothing pre-merge** (the platform PR/MR holds the review state); at
|
|
136
|
+
merge it re-reads approvals from the platform, advances the step, and flips the artifact status on
|
|
137
|
+
the **default branch** (the only place CI ever commits). CI is the sole ledger writer; front gates
|
|
138
|
+
stay permanently human.
|
|
116
139
|
- **The CI tokens are the one documented bend of "no stored tokens".** GitHub uses the platform's own
|
|
117
140
|
ephemeral `github.token` (nothing stored). GitLab requires a stored masked `SDLC_GATE_TOKEN`
|
|
118
141
|
project access token because `CI_JOB_TOKEN` can neither read the approvals API nor push — say so
|
|
119
142
|
when wiring, and scope it to `read_api` + `write_repository` only.
|
|
143
|
+
- **Protect the hub default branch.** Require that `epics/**` artifacts change only through a review
|
|
144
|
+
PR/MR (branch protection). This keeps revoke-on-change sound: it removes the only window where a
|
|
145
|
+
delayed reconcile could advance on an out-of-band post-merge artifact change (see `references/bridge.md`,
|
|
146
|
+
"Known limitation").
|
|
120
147
|
- **Degrade gracefully.** No platform / disabled bridge / no CLI → the gate runs file-only with no error.
|
|
121
148
|
|
|
122
149
|
## Reference
|
|
@@ -73,64 +73,106 @@ comments, replies, the reviewer **resolves** their thread, then `sync` runs agai
|
|
|
73
73
|
- **Merge → advance.** When the reviewer rule is satisfied, every thread is resolved, **and the review
|
|
74
74
|
PR/MR is merged**, `sync` marks the step `done` and unblocks the next step. The merge is the human
|
|
75
75
|
approval act — there is no separate machine advance. (`yad gate sync` performs this deterministically.)
|
|
76
|
-
- **Revoke on artifact change
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
76
|
+
- **Revoke on artifact change (checked at merge).** Path B reconciles at merge, so an approval given
|
|
77
|
+
to an earlier revision must not count for the merged content. How that is enforced differs by
|
|
78
|
+
platform:
|
|
79
|
+
- **GitHub — in code.** Each approval carries the **commit SHA it was made on**; `mapApprovers`
|
|
80
|
+
drops any approval whose commit ≠ the merged head (`headOid`). The reviewer approved an older
|
|
81
|
+
revision → their review is stale → dropped. No platform setting required.
|
|
82
|
+
- **GitLab — platform setting.** GitLab approvals expose no per-approval commit SHA (the reader omits
|
|
83
|
+
it), so the in-code SHA check does not apply and the approval is kept. Revoke-on-change there is the
|
|
84
|
+
platform's **"remove all approvals when commits are added to the source branch"** setting — **enable
|
|
85
|
+
it** (required for the guarantee on GitLab; safe under Path B because CI never pushes the source
|
|
86
|
+
branch, so only the owner's own artifact pushes drop approvals).
|
|
87
|
+
- **Degraded GitHub read — fail closed.** If the GitHub commit read fails (the reader returns a
|
|
88
|
+
`null` commit), the approval is **dropped**: a transient failure holds the gate rather than
|
|
89
|
+
advancing on approvals whose freshness cannot be proven (re-run `yad gate sync` to recover).
|
|
90
|
+
CHANGES_REQUESTED is still honored, so a degraded read can only ever *hold* the gate.
|
|
91
|
+
The `artifactHash` stamp still binds architecture approvals to the locked contract surface (see
|
|
92
|
+
"Contract re-lock" above).
|
|
93
|
+
- **Known limitation — protect the hub default branch.** The advance hashes the artifact from the
|
|
94
|
+
default branch as it stands when CI runs, while approvals are SHA-bound to the reviewed PR/MR head.
|
|
95
|
+
Those can differ if the artifact changes on the **base** outside this review while the PR/MR is open
|
|
96
|
+
(the merge then integrates a change the reviewers never saw) or if a later out-of-band commit edits
|
|
97
|
+
the merged artifact before a delayed reconcile advances it. In both cases each approval's commit
|
|
98
|
+
still equals the reviewed head, so the SHA check passes, yet the live content was not reviewed. Close
|
|
99
|
+
it operationally: **require branch protection on the hub default branch so `epics/**` artifacts can
|
|
100
|
+
only change through their own review PR/MR** (one open review per artifact) — then the base copy of an
|
|
101
|
+
artifact cannot move while its review is open, so the merged/live content always equals the reviewed
|
|
102
|
+
content. (The complete in-code fix would hash the artifact at the reviewed PR-head revision before
|
|
103
|
+
advancing; deferred in favor of the branch-protection mitigation.)
|
|
104
|
+
|
|
105
|
+
## Event-driven sync (hub CI) — Path B
|
|
106
|
+
|
|
107
|
+
The `wire` action (SKILL.md Step 4) installs CI on the hub so a **merge** drives `yad gate ci` —
|
|
108
|
+
**CI is the SOLE writer of the ledger, and it writes only at merge, only to the default branch.**
|
|
109
|
+
During review CI writes nothing: the platform PR/MR is the source of truth (native approvals +
|
|
110
|
+
threads). The CLI is self-sufficient at merge: it derives the epic + artifact from the
|
|
111
|
+
`review/EP-<slug>/<artifact-base>` head branch, takes the PR/MR number from the event (GitHub) or
|
|
112
|
+
resolves it from the platform (GitLab), upserts the `hub-prs.json` entry itself, and **re-reads
|
|
113
|
+
approvals fresh from the platform** — so no ledger needs to be pre-seeded on the branch.
|
|
114
|
+
|
|
115
|
+
| Platform event | Phase | CI action |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| PR/MR opened / reopened / pushed / reviewed | pre-merge | **none** — review state lives on the platform; CI never touches the branch |
|
|
118
|
+
| PR/MR closed **and merged** (the human act) | merge | `gate ci --branch <head> --pr <n> --merged` → re-read approvals, advance the step + flip the artifact `status:` **on the default branch** |
|
|
119
|
+
| Schedule (`*/15`) | reconcile | Safety net: enumerate recently-**merged** `review/EP-*` PRs/MRs via the API and advance any not yet `done` (idempotent). Recovers a merge whose merge-time run failed transiently, and on GitLab also picks up a squash merge whose commit dropped the branch name (and a bare approval — GitLab fires no pipeline on one). **GitHub:** a scheduled workflow, automatic once committed. **GitLab:** a pipeline schedule with `SDLC_GATE_SYNC=true` (one-time setup) |
|
|
120
|
+
|
|
121
|
+
**Why no pre-merge write fixes the gate.** Keeping CI off the PR head means an in-flight approval is
|
|
122
|
+
never dismissed by a CI commit, and the PR's required checks never strand on a `[skip ci]` CI commit.
|
|
123
|
+
Correctness is unaffected: at merge CI re-reads the PR/MR approvals from the platform and re-checks
|
|
124
|
+
each `artifactHash` against the merged content, so revoke-on-change still holds. The only ledger
|
|
125
|
+
commit — the advance plus the `draft → approved` status flip — lands on the **default branch** with
|
|
126
|
+
`[skip ci]`.
|
|
127
|
+
|
|
128
|
+
**The ledger is CI-owned (bridge mode only).** Humans never commit gate-state files: the `ledger-guard`
|
|
129
|
+
check (yad-checks) FAILs any commit on a review PR that touches `.sdlc/{state,approvals,comments,hub-prs}
|
|
130
|
+
.json` or `reviews/*.md` (`.sdlc/contract-lock.json` is artifact-side and allowed). Under Path B **no
|
|
131
|
+
CI commit lands in a review PR at all**, so the only ledger change the guard can see there is a human
|
|
132
|
+
edit — which it rejects. (The `verified-commits` gate still vets every commit's signature + author;
|
|
133
|
+
its gate-bot exemption is now vestigial in-PR because CI no longer commits there.) `yad gate open`
|
|
134
|
+
opens the PR only; local `yad gate sync` is advisory in bridge mode (writes nothing). After a merge,
|
|
135
|
+
everyone `git checkout <default> && git pull`. (Without the bridge, humans own the ledger locally and
|
|
136
|
+
these guards are no-ops.)
|
|
137
|
+
|
|
138
|
+
**Loop prevention & races.** The only ledger commit lands on the **default branch** at merge, which
|
|
139
|
+
fires no PR trigger; it carries `[skip ci]` to guard sibling workflows. Because CI never pushes the
|
|
140
|
+
review branch, there is no `synchronize` / MR-pipeline loop to prevent — and it is now **safe to enable**
|
|
141
|
+
the platform's **"dismiss stale approvals on push"** (GitHub) / **"remove all approvals when commits
|
|
142
|
+
are added to the source branch"** (GitLab): only the owner's own artifact pushes dismiss approvals,
|
|
143
|
+
which is exactly the intended revoke-on-change. Merge advances serialize on the default branch; the
|
|
144
|
+
push retries with a rebase.
|
|
110
145
|
|
|
111
146
|
**Tokens.**
|
|
112
147
|
- GitHub: the ephemeral `github.token` with `contents: write` + `pull-requests: read` — nothing stored.
|
|
148
|
+
Only the merge job pushes, and only the default branch.
|
|
113
149
|
- GitLab: a masked `SDLC_GATE_TOKEN` project access token (`read_api` + `write_repository`) — the one
|
|
114
150
|
documented bend of the no-stored-tokens rule; `CI_JOB_TOKEN` can neither read the approvals API nor
|
|
115
|
-
push.
|
|
116
|
-
- Protected default branch (GitHub):
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
151
|
+
push. Used only for the merge-time default-branch push + the API reads.
|
|
152
|
+
- Protected default branch (GitHub): the merge advance needs to push it — prefer a ruleset bypass for
|
|
153
|
+
Actions, else a fine-grained PAT as `SDLC_GATE_TOKEN` on the mergesync checkout.
|
|
154
|
+
|
|
155
|
+
**Manual sync & recovery.** In bridge mode `yad gate sync` is **advisory** (read-only) — it prints the
|
|
156
|
+
predicate but writes nothing, so it is **not** a recovery path when CI fails. If a merge-time run fails
|
|
157
|
+
(can't push, API hiccup), recovery is the scheduled **reconcile** job (automatic; it re-advances merged
|
|
158
|
+
reviews not yet `done`). To force it immediately, a maintainer runs the same command CI runs, locally on
|
|
159
|
+
the default branch: `yad gate ci --branch <review-branch> --pr <n> --merged` (this writes + pushes,
|
|
160
|
+
unlike advisory `yad gate sync`). File-only mode (no platform) keeps `yad gate sync` as the local writer.
|
|
161
|
+
The file ledger is still the source of truth.
|
|
123
162
|
|
|
124
163
|
### Manual end-to-end verification (GitHub)
|
|
125
164
|
|
|
126
165
|
1. On a scratch hub: `yad setup` (platform github, roster with a second account) → `yad check --fix`
|
|
127
166
|
installs `.github/workflows/yad-gate-sync.yml`; commit + push it.
|
|
128
|
-
2. Author an epic → `yad gate open EP-x epic.md` → the review PR opens.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
2. Author an epic → `yad gate open EP-x epic.md` → the review PR opens. CI writes nothing yet — review
|
|
168
|
+
state lives on the platform.
|
|
169
|
+
3. Second account **approves** / **requests changes** → no CI run touches the branch; the PR's native
|
|
170
|
+
approvals + threads are the source of truth (local `yad gate sync` is advisory and shows the
|
|
171
|
+
predicate without writing).
|
|
172
|
+
4. Resolve the thread, approve again, a human **merges** → the mergesync run advances `state.json`
|
|
173
|
+
(`epic-review: done`, `currentStep: architecture`) + flips `epic.md` to `approved` **on the
|
|
174
|
+
default branch**.
|
|
175
|
+
5. `git checkout <default> && git pull` locally — `yad gate status EP-x` matches the platform history.
|
|
176
|
+
|
|
177
|
+
GitLab variant: same flow on an MR; the advance lands on the merge push (or the next schedule tick if
|
|
178
|
+
a squash merge dropped the branch name).
|
|
@@ -1,52 +1,60 @@
|
|
|
1
1
|
# yad-managed: yad-hub-bridge
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
# into the file ledger (epics/<epic>/.sdlc/*.json + reviews/*.md) and commits ONLY those ledger
|
|
5
|
-
# files to the default branch. It never approves and never merges — the merge click remains the
|
|
6
|
-
# human approval act; the gate predicate is the same one `yad gate sync` runs locally.
|
|
2
|
+
# Merge-time gate sync for the PRODUCT HUB (Path B). CI is the SOLE writer of the ledger, and it
|
|
3
|
+
# writes ONLY at merge, ONLY to the default branch.
|
|
7
4
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
5
|
+
# During review there is NO CI write: the platform PR is the source of truth (native approvals +
|
|
6
|
+
# review threads). CI never pushes to the review branch, so an in-flight approval is never dismissed
|
|
7
|
+
# by a CI commit and the PR's required checks never strand on one. Because nothing is pushed to the
|
|
8
|
+
# PR head, you can safely enable "Dismiss stale pull request approvals when new commits are pushed"
|
|
9
|
+
# — only the artifact owner's own pushes will dismiss approvals (the intended revoke-on-change).
|
|
10
10
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
11
|
+
# Two jobs:
|
|
12
|
+
# MERGE (PR closed + merged): the fast path. Check out the default branch (the human merge already
|
|
13
|
+
# landed the artifact there) and run `yad gate ci --branch <head> --pr <n> --merged`. CI re-reads
|
|
14
|
+
# the PR's approvals fresh from the platform, advances the step, and flips the artifact `status:` to
|
|
15
|
+
# approved — committing that advance to the default branch with [skip ci].
|
|
16
|
+
#
|
|
17
|
+
# RECONCILE (schedule, every 15 min): the safety net. The merge job's `closed` event fires once and
|
|
18
|
+
# never repeats, so a transient API/GraphQL/push failure (or a fail-closed degraded approval read)
|
|
19
|
+
# would otherwise strand a merged review until someone reran it by hand. This periodic job discovers
|
|
20
|
+
# recently-merged review PRs from the API and advances any not yet done (idempotent — a step already
|
|
21
|
+
# `done` is skipped). On GitHub a scheduled workflow runs automatically once committed (no setup).
|
|
22
|
+
#
|
|
23
|
+
# CI never approves and never merges — the merge click is the human approval act.
|
|
24
|
+
#
|
|
25
|
+
# Protected default branch? Only these jobs push there. If the default branch is protected against
|
|
26
|
+
# Actions:
|
|
27
|
+
# a) add a ruleset bypass so GitHub Actions may push to it, or
|
|
28
|
+
# b) store a fine-grained PAT as the SDLC_GATE_TOKEN secret and pass it to actions/checkout below
|
|
29
|
+
# via `with: { token: ${{ secrets.SDLC_GATE_TOKEN }} }` — see references/bridge.md.
|
|
17
30
|
name: yad-gate-sync
|
|
18
31
|
on:
|
|
19
|
-
pull_request_review:
|
|
20
|
-
types: [submitted, dismissed]
|
|
21
32
|
pull_request:
|
|
22
|
-
types: [closed
|
|
33
|
+
types: [closed]
|
|
34
|
+
schedule:
|
|
35
|
+
- cron: "*/15 * * * *"
|
|
23
36
|
|
|
24
37
|
permissions:
|
|
25
|
-
contents: write # push the ledger commit
|
|
38
|
+
contents: write # push the merge-advance ledger commit to the default branch
|
|
26
39
|
pull-requests: read # gh pr view + reviewThreads GraphQL
|
|
27
40
|
|
|
28
|
-
# Serialize runs repo-wide so ledger pushes never race; sync reads the FULL platform state each
|
|
29
|
-
# time, so a queued superseded run loses nothing.
|
|
30
|
-
concurrency:
|
|
31
|
-
group: yad-gate-sync
|
|
32
|
-
cancel-in-progress: false
|
|
33
|
-
|
|
34
41
|
jobs:
|
|
35
|
-
|
|
42
|
+
mergesync:
|
|
43
|
+
# The human merge of a review branch: advance the step + flip the artifact status on the default
|
|
44
|
+
# branch. Nothing runs pre-merge — review state lives on the platform until the merge.
|
|
36
45
|
if: >
|
|
46
|
+
github.event_name == 'pull_request' &&
|
|
37
47
|
startsWith(github.event.pull_request.head.ref, 'review/EP-') &&
|
|
38
|
-
|
|
39
|
-
github.event.action == 'synchronize' ||
|
|
40
|
-
github.event.pull_request.merged == true)
|
|
48
|
+
github.event.pull_request.merged == true
|
|
41
49
|
runs-on: ubuntu-latest
|
|
50
|
+
# Serialize every advance with the reconcile job — they all push the default branch.
|
|
51
|
+
concurrency:
|
|
52
|
+
group: yad-gate-mergesync
|
|
53
|
+
cancel-in-progress: false
|
|
42
54
|
env:
|
|
43
55
|
GH_TOKEN: ${{ github.token }}
|
|
44
56
|
steps:
|
|
45
|
-
# Check out the
|
|
46
|
-
# overlays the artifact from the head ref itself so approvals bind to the reviewed content.
|
|
47
|
-
# A head branch auto-deleted on merge is fine: the event payload still carries head.ref (the
|
|
48
|
-
# string `gate ci` parses), and a failed fetch just skips the overlay — by then the artifact
|
|
49
|
-
# is already on the base branch via the merge.
|
|
57
|
+
# Check out the default branch — the merge already landed the artifact here.
|
|
50
58
|
- uses: actions/checkout@v4
|
|
51
59
|
with:
|
|
52
60
|
ref: ${{ github.event.pull_request.base.ref }}
|
|
@@ -54,10 +62,52 @@ jobs:
|
|
|
54
62
|
- uses: actions/setup-node@v4
|
|
55
63
|
with:
|
|
56
64
|
node-version: "20"
|
|
57
|
-
- name:
|
|
65
|
+
- name: Advance the gate on merge
|
|
58
66
|
run: |
|
|
59
67
|
git config user.name "yad-gate-sync[bot]"
|
|
60
68
|
git config user.email "yad-gate-sync[bot]@users.noreply.github.com"
|
|
61
|
-
npx -y -p yadflow@
|
|
69
|
+
npx -y -p yadflow@3 yad gate ci \
|
|
62
70
|
--branch "${{ github.event.pull_request.head.ref }}" \
|
|
63
|
-
--pr "${{ github.event.pull_request.number }}"
|
|
71
|
+
--pr "${{ github.event.pull_request.number }}" \
|
|
72
|
+
--merged
|
|
73
|
+
|
|
74
|
+
reconcile:
|
|
75
|
+
# Safety net: recover any merged review PR whose merge-time run failed transiently (the `closed`
|
|
76
|
+
# event never re-fires). Idempotent — `yad gate ci` skips a step that is already `done`.
|
|
77
|
+
if: github.event_name == 'schedule'
|
|
78
|
+
runs-on: ubuntu-latest
|
|
79
|
+
concurrency:
|
|
80
|
+
group: yad-gate-mergesync
|
|
81
|
+
cancel-in-progress: false
|
|
82
|
+
env:
|
|
83
|
+
GH_TOKEN: ${{ github.token }}
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
with:
|
|
87
|
+
ref: ${{ github.event.repository.default_branch }}
|
|
88
|
+
fetch-depth: 0
|
|
89
|
+
- uses: actions/setup-node@v4
|
|
90
|
+
with:
|
|
91
|
+
node-version: "20"
|
|
92
|
+
- name: Reconcile recently-merged review PRs
|
|
93
|
+
run: |
|
|
94
|
+
git config user.name "yad-gate-sync[bot]"
|
|
95
|
+
git config user.email "yad-gate-sync[bot]@users.noreply.github.com"
|
|
96
|
+
# A stuck merged review is always recent; window generously and let `gate ci` no-op the rest.
|
|
97
|
+
# Page the search API fully (no fixed result cap) so a stuck PR is never missed for volume.
|
|
98
|
+
# Aggregate failures and exit nonzero so a persistent API/push failure surfaces (red run)
|
|
99
|
+
# instead of silently stranding a merged gate behind a green safety-net. Read from a file,
|
|
100
|
+
# not a pipe, so the failure flag survives (a piped `while` runs in a subshell).
|
|
101
|
+
SINCE="$(date -u -d '7 days ago' +%Y-%m-%d 2>/dev/null || date -u -v-7d +%Y-%m-%d)"
|
|
102
|
+
rc=0
|
|
103
|
+
gh api --paginate -X GET search/issues \
|
|
104
|
+
--raw-field q="repo:${GITHUB_REPOSITORY} type:pr is:merged merged:>=${SINCE}" \
|
|
105
|
+
--jq '.items[].number' > /tmp/yad-merged-prs || rc=1
|
|
106
|
+
while read -r N; do
|
|
107
|
+
[ -n "$N" ] || continue
|
|
108
|
+
REF="$(gh pr view "$N" --json headRefName --jq '.headRefName' 2>/dev/null)" || rc=1
|
|
109
|
+
case "$REF" in
|
|
110
|
+
review/EP-*) npx -y -p yadflow@3 yad gate ci --branch "$REF" --pr "$N" --merged || rc=1 ;;
|
|
111
|
+
esac
|
|
112
|
+
done < /tmp/yad-merged-prs
|
|
113
|
+
exit $rc
|