yadflow 2.14.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/bin/yad.mjs +19 -5
- package/cli/artifact-status.mjs +102 -0
- package/cli/doctor.mjs +14 -1
- package/cli/epic-state.mjs +1 -1
- package/cli/gate.mjs +121 -58
- package/cli/manifest.mjs +2 -0
- package/cli/platform.mjs +44 -6
- package/package.json +1 -1
- 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-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
|
@@ -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
|
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
# yad-managed-include: yad-hub-bridge
|
|
2
|
-
#
|
|
3
|
-
# root .gitlab-ci.yml via:
|
|
2
|
+
# Merge-time gate sync for the PRODUCT HUB, as an INCLUDABLE fragment (Path B). Pulled into the
|
|
3
|
+
# hub's root .gitlab-ci.yml via:
|
|
4
4
|
# include:
|
|
5
5
|
# - local: '.gitlab/ci/yad-gate-sync.yml'
|
|
6
6
|
# so wiring never edits the foreign root pipeline beyond that one include line. The job carries
|
|
7
7
|
# `needs: []` and no `stage:` (same merge-safety as the yad-checks fragment).
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# the
|
|
9
|
+
# CI is the SOLE writer of the ledger, and it writes ONLY at merge, ONLY to the default branch.
|
|
10
|
+
# During review there is NO CI write: the MR itself is the source of truth (native approvals +
|
|
11
|
+
# threads). CI never pushes to the MR source (review) branch, so an in-flight approval is never
|
|
12
|
+
# dismissed by a CI commit. Because nothing is pushed to the source branch, you can safely enable
|
|
13
|
+
# "Remove all approvals when commits are added to the source branch" — only the artifact owner's own
|
|
14
|
+
# pushes will dismiss approvals (the intended revoke-on-change).
|
|
13
15
|
#
|
|
14
|
-
# GitLab is the DEGRADED
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# GitLab is the DEGRADED platform, stated honestly: it fires no pipeline on an approval, and the
|
|
17
|
+
# merge push to the default branch may omit the source-branch name (squash / fast-forward). So the
|
|
18
|
+
# ledger is reconciled from the PLATFORM (glab API), not from any per-branch ledger:
|
|
19
|
+
# - MERGE push (default branch, commit names a review branch): resolve the merged MR's IID from its
|
|
20
|
+
# source branch, then advance + flip status on the default branch — near-immediate.
|
|
21
|
+
# - SCHEDULED sweep (catch-up): enumerate recently-merged review MRs via the API and advance any
|
|
22
|
+
# not yet advanced (idempotent — a step already `done` is skipped). Covers bare approvals and
|
|
23
|
+
# squash merges whose commit message dropped the branch name. Create a pipeline schedule
|
|
24
|
+
# (one-time, cannot be committed as code): cron `*/15 * * * *` with variable SDLC_GATE_SYNC=true.
|
|
25
|
+
# UI: CI/CD > Schedules, or:
|
|
19
26
|
# glab api projects/:id/pipeline_schedules -X POST \
|
|
20
27
|
# -f description='yad gate sync' -f ref=main -f cron='*/15 * * * *'
|
|
21
28
|
# glab api "projects/:id/pipeline_schedules/<id>/variables" -X POST \
|
|
22
29
|
# -f key=SDLC_GATE_SYNC -f value=true
|
|
23
30
|
#
|
|
24
|
-
# Token
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
31
|
+
# Token: CI_JOB_TOKEN can neither read the approvals API nor push to a protected branch. Create a
|
|
32
|
+
# PROJECT ACCESS TOKEN with `read_api` + `write_repository` (role Developer+, allowed to push to the
|
|
33
|
+
# default branch — the merge advance pushes there) and store it as a masked CI/CD variable
|
|
34
|
+
# SDLC_GATE_TOKEN. Without it the job fails visibly; recover by setting the token, then re-run the
|
|
35
|
+
# pipeline or run `yad gate ci --branch <review-branch> --pr <iid> --merged` locally on the default
|
|
36
|
+
# branch (advisory `yad gate sync` is read-only in bridge mode and cannot recover a stuck gate).
|
|
29
37
|
variables:
|
|
30
|
-
GIT_DEPTH: "0" # full history: gate ci
|
|
38
|
+
GIT_DEPTH: "0" # full history: gate ci pushes the advance to the default branch
|
|
31
39
|
|
|
32
40
|
yad-gate-sync:
|
|
33
41
|
needs: []
|
|
@@ -38,32 +46,55 @@ yad-gate-sync:
|
|
|
38
46
|
tags: [$YAD_RUNNER_TAGS]
|
|
39
47
|
image: node:20
|
|
40
48
|
rules:
|
|
41
|
-
# MR-event path: fires on MR open / push to the review branch — NOT on approval (see header).
|
|
42
|
-
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^review\/EP-/
|
|
43
49
|
# Merge path: branch pipeline on the default branch whose merge commit names a review branch.
|
|
44
50
|
# Squash/fast-forward merges may omit the branch name — those advance on the next scheduled sweep.
|
|
45
51
|
- if: $CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_MESSAGE =~ /review\/EP-/
|
|
46
|
-
# Scheduled sweep: the only path that picks up approvals.
|
|
52
|
+
# Scheduled sweep: the only path that picks up bare approvals (advances merged reviews it finds).
|
|
47
53
|
- if: $CI_PIPELINE_SOURCE == "schedule" && $SDLC_GATE_SYNC == "true"
|
|
48
54
|
script:
|
|
49
55
|
# Pinned glab binary (node:20 has no glab). Alternative: image registry.gitlab.com/gitlab-org/cli.
|
|
50
56
|
- GLAB_VERSION=1.55.0
|
|
51
57
|
- curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_amd64.deb" -o /tmp/glab.deb && dpkg -i /tmp/glab.deb
|
|
52
|
-
# The ledger lives on the default branch; gate ci overlays the artifact from the review branch itself.
|
|
53
|
-
- git fetch origin "$CI_DEFAULT_BRANCH"
|
|
54
|
-
- git checkout -B "$CI_DEFAULT_BRANCH" "origin/$CI_DEFAULT_BRANCH"
|
|
55
58
|
- git config user.name "yad-gate-sync" && git config user.email "yad-gate-sync@noreply.${CI_SERVER_HOST}"
|
|
56
59
|
- git remote set-url origin "https://oauth2:${SDLC_GATE_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
|
|
57
60
|
- export GITLAB_TOKEN="$SDLC_GATE_TOKEN" GITLAB_HOST="$CI_SERVER_URL"
|
|
61
|
+
- git fetch origin "$CI_DEFAULT_BRANCH"
|
|
62
|
+
- git checkout -B "$CI_DEFAULT_BRANCH" "origin/$CI_DEFAULT_BRANCH"
|
|
58
63
|
- |
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
rc=0
|
|
65
|
+
if [ "$CI_PIPELINE_SOURCE" = "schedule" ]; then
|
|
66
|
+
# SCHEDULED SWEEP — discover merged review MRs from the platform (Path B keeps no per-branch
|
|
67
|
+
# ledger), then advance each. `gate ci` is idempotent: a step already `done` is skipped.
|
|
68
|
+
# A stuck review MR (a squash merge whose commit dropped the branch name, or a failed merge
|
|
69
|
+
# push) is always RECENT, so sweep a generous recent window and PAGINATE it fully (--paginate)
|
|
70
|
+
# — this bounds cost without the old hard 50-row cap that could permanently strand older MRs.
|
|
71
|
+
# Aggregate failures into rc and exit nonzero so a persistent failure surfaces (red pipeline)
|
|
72
|
+
# instead of silently stranding a merged gate. Read from a file, not a pipe (a piped `while`
|
|
73
|
+
# runs in a subshell, losing rc). An MR stuck beyond the window needs manual recovery — run
|
|
74
|
+
# `yad gate ci --branch <review-branch> --pr <iid> --merged` locally on the default branch.
|
|
75
|
+
SINCE="$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-7d +%Y-%m-%dT%H:%M:%SZ)"
|
|
76
|
+
glab api --paginate "projects/:id/merge_requests?state=merged&updated_after=${SINCE}&per_page=100&order_by=updated_at" \
|
|
77
|
+
--jq '.[] | select(.source_branch | startswith("review/EP-")) | "\(.iid) \(.source_branch)"' > /tmp/yad-merged-mrs || rc=1
|
|
78
|
+
while read -r IID REF; do
|
|
79
|
+
[ -n "$IID" ] || continue
|
|
80
|
+
git checkout -q -B "$CI_DEFAULT_BRANCH" "origin/$CI_DEFAULT_BRANCH"
|
|
81
|
+
npx -y -p yadflow@3 yad gate ci --branch "$REF" --pr "$IID" --merged || rc=1
|
|
82
|
+
done < /tmp/yad-merged-mrs
|
|
67
83
|
else
|
|
68
|
-
|
|
84
|
+
# MERGE push to the default branch whose commit names a review branch: resolve the merged MR's
|
|
85
|
+
# IID from its source branch so `gate ci` can re-read approvals, then advance there.
|
|
86
|
+
REVIEW_BRANCH="$(printf '%s' "$CI_COMMIT_MESSAGE" | grep -oE 'review/EP-[A-Za-z0-9._/-]+' | head -n1 || true)"
|
|
87
|
+
if [ -n "$REVIEW_BRANCH" ]; then
|
|
88
|
+
IID="$(glab api "projects/:id/merge_requests?source_branch=${REVIEW_BRANCH}&state=merged" --jq '.[0].iid' 2>/dev/null || true)"
|
|
89
|
+
if [ -n "$IID" ]; then
|
|
90
|
+
# Pass --pr + IID as two distinct args (avoid a fragile, shell-dependent ${IID:+...} split).
|
|
91
|
+
npx -y -p yadflow@3 yad gate ci --branch "$REVIEW_BRANCH" --pr "$IID" --merged || rc=1
|
|
92
|
+
else
|
|
93
|
+
# Without the IID, gate ci cannot re-read approvals — fail visibly (the scheduled sweep
|
|
94
|
+
# retries) rather than running a green no-op that silently leaves the gate unadvanced.
|
|
95
|
+
echo "yad-gate-sync: could not resolve merged MR IID for ${REVIEW_BRANCH}" >&2
|
|
96
|
+
rc=1
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
69
99
|
fi
|
|
100
|
+
exit $rc
|
|
@@ -165,21 +165,23 @@ If the predicate **passes**:
|
|
|
165
165
|
now `ready-for-build`, with `test-cases` running in parallel).
|
|
166
166
|
|
|
167
167
|
### PR-driven automation (the `yad gate` CLI)
|
|
168
|
-
When the hub has a platform, the
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
When the hub has a platform, **CI is the sole writer of the ledger**. `yad gate open` opens the review
|
|
169
|
+
PR only; CI (`yad gate ci`) writes the `.sdlc/` + `reviews/` records this skill describes. The skill's
|
|
170
|
+
job is the human half: presenting the artifact, helping the owner address comments, and narrating the
|
|
171
|
+
gate. Local `yad gate sync` is advisory in bridge mode (reads the platform, prints status, writes
|
|
172
|
+
nothing); a human must never commit gate-state files (the `ledger-guard` check rejects it).
|
|
173
173
|
|
|
174
174
|
Under that CLI the gate **advances on merge**: a review PR/MR whose reviewer rule is satisfied, whose
|
|
175
175
|
comment threads are **all resolved**, and which has been **merged** auto-marks the step `done` and
|
|
176
176
|
unblocks the next step. (Until those three hold, the step stays `in_review`.)
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
The flow is **merge-driven** (wired by `yad-hub-bridge` `wire`): during review CI writes nothing — the
|
|
179
|
+
platform PR/MR is the source of truth (native approvals + threads), and CI never touches the review
|
|
180
|
+
branch (so an in-flight approval is never dismissed and required checks never strand). On the human
|
|
181
|
+
**merge** CI re-reads approvals, advances the step, and flips the artifact `status:` on the **default
|
|
182
|
+
branch**. After a merge, `git checkout <default> && git pull` to see it. The predicate and the human
|
|
183
|
+
merge are unchanged — CI never approves and never merges. File-only mode (no platform) keeps the local
|
|
184
|
+
write path.
|
|
183
185
|
|
|
184
186
|
### Hard rules (build plan §1, §5)
|
|
185
187
|
- **The merge click is the human approval act.** A front step advances only when a human merges the
|