yadflow 2.15.0 → 2.16.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 +3 -3
- package/README.md +2 -2
- package/bin/yad.mjs +5 -3
- package/cli/commit.mjs +9 -2
- package/cli/gate.mjs +8 -3
- package/cli/openpr.mjs +78 -6
- package/package.json +1 -1
- package/skills/yad-checks/references/check-gates.md +25 -7
- package/skills/yad-checks/templates/github/yad-hub-checks.yml +27 -6
- package/skills/yad-checks/templates/gitlab/yad-hub-checks.gitlab-ci.yml +11 -5
- package/skills/yad-commit/SKILL.md +2 -1
- package/skills/yad-open-pr/SKILL.md +8 -0
- package/skills/yad-pr-template/templates/checks/pr-template.sh +53 -10
- package/skills/yad-pr-template/templates/checks/pr-title.sh +55 -18
- package/skills/yad-ship/SKILL.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
## [2.16.1](https://github.com/abdelrahmannasr/yadflow/compare/v2.16.0...v2.16.1) (2026-06-25)
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
###
|
|
4
|
+
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* **open-pr:** make build helpers stage-aware on the hub (closes [#80](https://github.com/abdelrahmannasr/yadflow/issues/80)) ([#81](https://github.com/abdelrahmannasr/yadflow/issues/81)) ([8d74e3d](https://github.com/abdelrahmannasr/yadflow/commit/8d74e3d267d4c056992d3bc6f5a2a7a15a66b431))
|
|
7
7
|
|
|
8
8
|
# [2.2.0](https://github.com/abdelrahmannasr/yadflow/compare/v2.1.0...v2.2.0) (2026-06-14)
|
|
9
9
|
|
package/README.md
CHANGED
|
@@ -98,8 +98,8 @@ with `npx` from your **product hub** repo — no clone needed.
|
|
|
98
98
|
| `yad gate status <epic>` | Show each review step and its recorded approvals. |
|
|
99
99
|
| `yad gate ci [--branch <head>] [--pr <n>]` | The CI entry the hub workflow calls on review/merge events: derive the epic/artifact from the `review/EP-*` branch, run the same sync, and commit **only the ledger** to the hub default branch (sweep every open review PR when no `--branch`). |
|
|
100
100
|
| `yad commit --type <t> -m <subject>` | Commit by the SDLC convention — Conventional subject, `Task`/`Contract-Change`/`Co-Authored-By` trailers, atomic-file guard. |
|
|
101
|
-
| `yad open-pr [--repo <name>]` | Open a
|
|
102
|
-
| `yad ship --type <t> -m <subject>` | Commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`)
|
|
101
|
+
| `yad open-pr [--repo <name>]` | Open a **task** PR/MR from the platform template (build half). **Stage-aware on the hub:** a `review/EP-*` branch opens the front-half artifact-review PR (delegates to `yad gate open`); any other hub branch uses the code-task template (so hub tooling PRs pass the `pr-template` gate). |
|
|
102
|
+
| `yad ship --type <t> -m <subject>` | Commit **and** open the task PR/MR in one step (`yad commit` then `yad open-pr`) — stage-aware, same as `open-pr`. |
|
|
103
103
|
| `yad repo list` / `yad repo refresh [name]` | List connected repos as **fresh / stale**, and re-pack a stale one — staleness is now an explicit human decision, never an automatic skill side-effect. |
|
|
104
104
|
| `yad repo sync [name]` | Switch every connected repo to its **default branch** and fast-forward it from origin (one or all). Dirty repos are skipped, never overwritten; fast-forward only. |
|
|
105
105
|
| `npx yadflow --version` | Print the installed CLI version. |
|
package/bin/yad.mjs
CHANGED
|
@@ -56,8 +56,10 @@ ${c.bold('Review gate (front half)')}
|
|
|
56
56
|
|
|
57
57
|
${c.bold('Build helpers')}
|
|
58
58
|
yad commit --type <t> -m <subject> Commit by convention (trailers, atomic guard)
|
|
59
|
-
yad open-pr [--repo <name>] Open a
|
|
60
|
-
|
|
59
|
+
yad open-pr [--repo <name>] Open a task PR/MR — stage-aware on the hub: a review/EP-*
|
|
60
|
+
branch opens the front-half artifact-review PR (delegates to
|
|
61
|
+
gate open), any other hub branch uses the code-task template
|
|
62
|
+
yad ship --type <t> -m <subject> Commit AND open the task PR/MR in one step (stage-aware)
|
|
61
63
|
yad repo list Show connected repos (fresh / stale)
|
|
62
64
|
yad repo refresh [name] Re-pack a stale repo (a human decision)
|
|
63
65
|
|
|
@@ -181,7 +183,7 @@ async function main() {
|
|
|
181
183
|
// In bridge mode CI is the sole ledger writer: `open` only opens the PR, and local `sync` is
|
|
182
184
|
// advisory (reads the platform, prints status, writes nothing). The artifact status flip is
|
|
183
185
|
// CI's job at merge — never wired into the local gate. File-only mode keeps local writes.
|
|
184
|
-
if (action === 'open') await gateOpen(o.dir, { epic, artifact
|
|
186
|
+
if (action === 'open') await gateOpen(o.dir, { epic, artifact });
|
|
185
187
|
else if (action === 'sync') await gateSync(o.dir, { epic, artifact, today, local: true });
|
|
186
188
|
else if (action === 'comments') await gateComments(o.dir, { epic, artifact, today });
|
|
187
189
|
else if (action === 'status') await gateStatus(o.dir, { epic });
|
package/cli/commit.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import fs from 'node:fs';
|
|
|
7
7
|
import { c, log, ok, info, warn, fail, run, exists } from './lib.mjs';
|
|
8
8
|
import {
|
|
9
9
|
COMMIT_TYPES, AI_COAUTHORS, ATOMIC_FILE_LIMIT,
|
|
10
|
-
TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER,
|
|
10
|
+
TASK_TRAILER, CONTRACT_CHANGE_TRAILER, COAUTHOR_TRAILER, PROJECT_FILES,
|
|
11
11
|
} from './manifest.mjs';
|
|
12
12
|
|
|
13
13
|
// PURE — unit tested directly. Build the full commit message text.
|
|
@@ -51,7 +51,14 @@ export async function runCommit(root, opts = {}) {
|
|
|
51
51
|
|
|
52
52
|
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root }).stdout;
|
|
53
53
|
const task = opts.task || taskFromBranch(branch);
|
|
54
|
-
if (!task)
|
|
54
|
+
if (!task) {
|
|
55
|
+
// spec-link is a code-repo gate (REPO_WIRING.common), not a hub gate — so a missing Task trailer
|
|
56
|
+
// is expected on a hub PR (front-half artifact review or hub tooling) and only matters on a repo.
|
|
57
|
+
const onHub = exists(path.join(root, PROJECT_FILES.hubConfig));
|
|
58
|
+
warn(onHub
|
|
59
|
+
? 'no Task trailer (none given and branch has no -S0N-T0N) — fine for a hub PR; required on code-repo tasks (spec-link gate)'
|
|
60
|
+
: 'no Task trailer (none given and branch has no -S0N-T0N) — spec-link gate will fail on a code repo');
|
|
61
|
+
}
|
|
55
62
|
|
|
56
63
|
let message;
|
|
57
64
|
try {
|
package/cli/gate.mjs
CHANGED
|
@@ -449,7 +449,11 @@ export async function gateStatus(root, { epic } = {}) {
|
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
|
|
452
|
+
// `head` overrides the review branch the PR is opened against — `open-pr` delegates here after pushing
|
|
453
|
+
// the user's checked-out branch, which for a per-story review (review/EP-*/stories-S01) does NOT equal
|
|
454
|
+
// the branch this would otherwise recompute (artifactFromBase collapses stories-S01 → stories/). Pass
|
|
455
|
+
// the real pushed head so the PR targets a branch that exists. `creator` is injected in tests.
|
|
456
|
+
export async function gateOpen(root, { epic, artifact, head, creator = createPr } = {}) {
|
|
453
457
|
const { hub } = loadHub(root);
|
|
454
458
|
const epicDir = epicRoot(root, epic);
|
|
455
459
|
const ledger = loadLedger(epicDir);
|
|
@@ -458,7 +462,7 @@ export async function gateOpen(root, { epic, artifact } = {}) {
|
|
|
458
462
|
const step = findReviewStep(ledger.state, artifact);
|
|
459
463
|
if (!step) { fail(`no review step for ${artifact}`); process.exitCode = 1; return; }
|
|
460
464
|
const b = base(artifact);
|
|
461
|
-
const branch = `review/${epic}/${b}`;
|
|
465
|
+
const branch = head || `review/${epic}/${b}`;
|
|
462
466
|
const domains = touchedDomains(epicDir, step);
|
|
463
467
|
warnUnlockedContract(epicDir, artifact);
|
|
464
468
|
|
|
@@ -487,7 +491,7 @@ export async function gateOpen(root, { epic, artifact } = {}) {
|
|
|
487
491
|
const assignees = committer ? [committer] : [];
|
|
488
492
|
const labels = isEscalated(step) ? domains.map((d) => `domain:${d}`) : [];
|
|
489
493
|
info(`opening review ${hub.platform === 'gitlab' ? 'MR' : 'PR'} on branch ${branch} …`);
|
|
490
|
-
const r =
|
|
494
|
+
const r = creator(hub.platform, { title: `review: ${artifact} (${epic})`, body, base: hub.default_branch || 'main', head: branch, reviewers, assignees, labels, cwd: root });
|
|
491
495
|
if (!r.ok) { warn(`could not open PR (${r.reason || 'unknown'})${bridge ? ' — open it manually; CI records the gate on merge' : '; step is in_review file-only'}`); return; }
|
|
492
496
|
|
|
493
497
|
if (!bridge) {
|
|
@@ -498,6 +502,7 @@ export async function gateOpen(root, { epic, artifact } = {}) {
|
|
|
498
502
|
hand(bridge
|
|
499
503
|
? 'reviewers approve/comment there; CI advances the gate on the default branch when it is merged'
|
|
500
504
|
: `reviewers approve/comment there; then run \`yad gate sync ${epic} ${artifact}\``);
|
|
505
|
+
return { url: r.url };
|
|
501
506
|
}
|
|
502
507
|
|
|
503
508
|
// ---- helpers ------------------------------------------------------------------------------------
|
package/cli/openpr.mjs
CHANGED
|
@@ -8,6 +8,8 @@ import { c, log, ok, info, hand, fail, run, exists, readJSON } from './lib.mjs';
|
|
|
8
8
|
import { PROJECT_FILES } from './manifest.mjs';
|
|
9
9
|
import { detectPlatform, createPr, reviewersForScopes, resolveCommitterLogin } from './platform.mjs';
|
|
10
10
|
import { taskFromBranch } from './commit.mjs';
|
|
11
|
+
import { parseReviewBranch, artifactFromBase } from './epic-state.mjs';
|
|
12
|
+
import { gateOpen } from './gate.mjs';
|
|
11
13
|
|
|
12
14
|
// Resolve the target code repo: --repo <name> from the registry, else --dir, else cwd.
|
|
13
15
|
function resolveRepo(root, { repo, dir }) {
|
|
@@ -19,11 +21,59 @@ function resolveRepo(root, { repo, dir }) {
|
|
|
19
21
|
return { repoRoot: path.resolve(root, dir || '.'), meta: null };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Which SDLC stage is this PR? The hub serves two vehicles; a code repo only one. Mirrors the
|
|
25
|
+
// `--head` split the hub pattern gates (pr-title.sh/pr-template.sh) already apply:
|
|
26
|
+
// code-repo — NOT the product hub (a registry repo via --repo, or root is not a hub).
|
|
27
|
+
// hub-front — the hub itself AND head is a review/EP-* branch (artifact-review PR).
|
|
28
|
+
// hub-tooling — the hub itself AND head is anything else (a tooling/CI change to the hub).
|
|
29
|
+
// `meta` (truthy when resolved from the repos registry via --repo) is a connected code repo, so it is
|
|
30
|
+
// never the hub regardless of its path. Otherwise "is the hub" = repoRoot resolves to root AND root
|
|
31
|
+
// carries .sdlc/hub.json. path.resolve normalises `--dir .` / trailing slashes.
|
|
32
|
+
export function detectStage(root, repoRoot, head, meta) {
|
|
33
|
+
if (meta) return 'code-repo';
|
|
34
|
+
const isHub = path.resolve(repoRoot) === path.resolve(root)
|
|
35
|
+
&& exists(path.join(root, PROJECT_FILES.hubConfig));
|
|
36
|
+
if (!isHub) return 'code-repo';
|
|
37
|
+
return /^review\/EP-[a-z0-9-]+\//.test(head || '') ? 'hub-front' : 'hub-tooling';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The bundled code-task template — the same file `REPO_WIRING` installs into code repos, resolved
|
|
41
|
+
// from the package (mirrors how manifest.mjs reads ../package.json). Used for a hub-tooling PR, whose
|
|
42
|
+
// `.github/pull_request_template.md` is the ARTIFACT-REVIEW template (wrong shape for the code-task
|
|
43
|
+
// hub gate). Falls back to a minimal body that still carries every section the gate requires.
|
|
44
|
+
function codeTaskTemplate(platform) {
|
|
45
|
+
const rel = platform === 'gitlab'
|
|
46
|
+
? '../skills/yad-pr-template/templates/gitlab/merge_request_templates/Default.md'
|
|
47
|
+
: '../skills/yad-pr-template/templates/github/pull_request_template.md';
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(new URL(rel, import.meta.url), 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
return [
|
|
52
|
+
'## Summary', '',
|
|
53
|
+
'## Impact & Risk',
|
|
54
|
+
'- **Domains / repos touched:** <repo>',
|
|
55
|
+
'- **Contract surface touched:** no',
|
|
56
|
+
'- **Risk level:** low',
|
|
57
|
+
'',
|
|
58
|
+
'## Checklist',
|
|
59
|
+
'- [ ] Lint, build, and tests pass (build/test/lint gate)',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function templateBody(repoRoot, platform, { task, risk, contract, domains, stage }) {
|
|
66
|
+
// hub-tooling: the hub's own template is artifact-review — use the bundled code-task template so the
|
|
67
|
+
// body matches the shape the hub `pr-template` gate demands for a non-review head.
|
|
68
|
+
let base;
|
|
69
|
+
if (stage === 'hub-tooling') {
|
|
70
|
+
base = codeTaskTemplate(platform);
|
|
71
|
+
} else {
|
|
72
|
+
const tplPath = platform === 'gitlab'
|
|
73
|
+
? path.join(repoRoot, '.gitlab/merge_request_templates/Default.md')
|
|
74
|
+
: path.join(repoRoot, '.github/pull_request_template.md');
|
|
75
|
+
base = exists(tplPath) ? fs.readFileSync(tplPath, 'utf8') : codeTaskTemplate(platform);
|
|
76
|
+
}
|
|
27
77
|
// Fill the obvious fields; leave the rest of the committed template intact for the author.
|
|
28
78
|
return base
|
|
29
79
|
.replace(/EP-<slug>-S0N-T0N/g, task || 'EP-<slug>-S0N-T0N')
|
|
@@ -45,6 +95,28 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
45
95
|
const baseBranch = opts.base || meta?.default_branch || 'main';
|
|
46
96
|
if (branch === baseBranch) { fail(`on ${baseBranch} — switch to your task branch first`); process.exitCode = 1; return; }
|
|
47
97
|
|
|
98
|
+
const stage = detectStage(root, repoRoot, branch, meta);
|
|
99
|
+
|
|
100
|
+
// hub-front: this is a front-half artifact-review PR (review/EP-*/<artifact> head on the hub). The
|
|
101
|
+
// artifact-review title, body, and ledger bookkeeping all live in `yad gate open` — delegate to it
|
|
102
|
+
// rather than emit the code-task shape (which the hub gate would reject). Push first (gateOpen does
|
|
103
|
+
// not push), then hand off; any --title/--message is dropped (gateOpen sets `review: …`).
|
|
104
|
+
if (stage === 'hub-front') {
|
|
105
|
+
const parsed = parseReviewBranch(branch);
|
|
106
|
+
if (!parsed) { fail(`could not parse review branch '${branch}' (expected review/EP-<slug>/<artifact>)`); process.exitCode = 1; return; }
|
|
107
|
+
info(`pushing ${branch} …`);
|
|
108
|
+
const fpush = run('git', ['push', '-u', 'origin', branch], { cwd: repoRoot });
|
|
109
|
+
if (!fpush.ok) { fail(`git push failed — ${fpush.stderr.split('\n')[0] || 'unknown'}`); process.exitCode = 1; return; }
|
|
110
|
+
// Pass the branch we just pushed as the head so gateOpen opens the PR against it (its own
|
|
111
|
+
// recompute would collapse a per-story base). gateOpen signals failure by returning no url —
|
|
112
|
+
// mirror open-pr's own error contract so `ship` sees the non-zero exit and never reports success.
|
|
113
|
+
// (On a platform-less hub gateOpen marks the step in_review file-only and returns no url; open-pr's
|
|
114
|
+
// job is to open a PR, so "no PR opened" is a non-zero outcome here, unlike `yad gate open`.)
|
|
115
|
+
const res = await gateOpen(root, { epic: parsed.epic, artifact: artifactFromBase(parsed.base), head: branch });
|
|
116
|
+
if (!res?.url) process.exitCode = 1;
|
|
117
|
+
return res;
|
|
118
|
+
}
|
|
119
|
+
|
|
48
120
|
// Push the branch (sets upstream) using the user's own auth. Abort on failure — creating a PR for a
|
|
49
121
|
// branch that is not on the remote just fails with a more confusing error.
|
|
50
122
|
info(`pushing ${branch} …`);
|
|
@@ -54,7 +126,7 @@ export async function runOpenPr(root, opts = {}) {
|
|
|
54
126
|
const task = opts.task || taskFromBranch(branch);
|
|
55
127
|
const title = opts.title || run('git', ['log', '-1', '--format=%s'], { cwd: repoRoot }).stdout || `task ${task || branch}`;
|
|
56
128
|
const body = templateBody(repoRoot, platform, {
|
|
57
|
-
task, risk: opts.risk || 'low', contract: !!opts.contractChange, domains: meta?.name,
|
|
129
|
+
task, risk: opts.risk || 'low', contract: !!opts.contractChange, domains: meta?.name, stage,
|
|
58
130
|
});
|
|
59
131
|
|
|
60
132
|
// Auto-assign from the hub roster, scoped to this repo: assignee = the committer (resolved from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.1",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo). A BMAD module + 30 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -121,8 +121,17 @@ from the event payload):
|
|
|
121
121
|
- `--profile code` (default) → a Conventional-Commits subject `<type>: <description>`, no trailing
|
|
122
122
|
period (`config.yaml build.pr_title_style: same_as_commit_subject` — one task = one PR, the title is
|
|
123
123
|
the squash-merge subject).
|
|
124
|
-
- `--profile hub` →
|
|
125
|
-
`
|
|
124
|
+
- `--profile hub` → splits by the PR/MR **head branch** (passed via `--head`, injected by CI):
|
|
125
|
+
- `review/EP-*` head (or no `--head` — stays strict) → a front-half artifact-review title
|
|
126
|
+
`review: <artifact> (EP-<slug>)`, the shape `yad gate open` creates.
|
|
127
|
+
- any other head → a tooling/code change to the hub itself, so it follows the `code` convention (a
|
|
128
|
+
Conventional-Commits subject). This is what lets a PR that changes the hub's own workflows/checks
|
|
129
|
+
pass — it has no EP artifact to review.
|
|
130
|
+
- **Anti-bypass guard.** The branch name alone is not trusted: a non-review head that actually
|
|
131
|
+
changes front-half artifacts (any path under `epics/**`) **FAILS** — those changes must go through
|
|
132
|
+
a `review/EP-*` PR and the artifact-review workflow. CI passes the PR's changed paths via
|
|
133
|
+
`--changed <file>` (computed from the diff against the base ref); without that list (a direct
|
|
134
|
+
by-hand caller) the guard is inert and the branch split alone applies.
|
|
126
135
|
|
|
127
136
|
## 7. pr-template (`templates/checks/pr-template.sh`)
|
|
128
137
|
|
|
@@ -131,8 +140,14 @@ catches a free-form description that bypassed it:
|
|
|
131
140
|
|
|
132
141
|
- `--profile code` (default) → requires `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled
|
|
133
142
|
`Risk level:` (`low|medium|high`).
|
|
134
|
-
- `--profile hub` →
|
|
135
|
-
|
|
143
|
+
- `--profile hub` → splits by the PR/MR **head branch** (passed via `--head`, injected by CI):
|
|
144
|
+
- `review/EP-*` head (or no `--head`) → requires the artifact-review template: `## Artifact under
|
|
145
|
+
review`, `## Impact & Risk (front-half)`, `## Checklist`, and a `Risk tags:` line.
|
|
146
|
+
- any other head → a hub tooling PR, so it requires the `code` task template (`## Summary`,
|
|
147
|
+
`## Impact & Risk`, `## Checklist`, filled `Risk level:`).
|
|
148
|
+
- **Anti-bypass guard** (same as pr-title): a non-review head that changes front-half artifacts
|
|
149
|
+
(`epics/**`, detected from the CI-supplied `--changed <file>` list) **FAILS** — artifact changes
|
|
150
|
+
must go through a `review/EP-*` PR.
|
|
136
151
|
|
|
137
152
|
## CI wiring (both platforms)
|
|
138
153
|
|
|
@@ -202,9 +217,12 @@ its one include line) whenever `.sdlc/hub.json` has a platform with the bridge e
|
|
|
202
217
|
front-half review PRs are held to the same rule as code-repo PRs: signed, known authors only.
|
|
203
218
|
|
|
204
219
|
The hub **also** runs the three pattern gates (`commit-message`, `pr-title`, `pr-template`) with
|
|
205
|
-
`--profile hub
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
`--profile hub`. The pattern gates split by the PR/MR **head branch** (passed via `--head`): a
|
|
221
|
+
`review/EP-*` head is a front-half review PR — Conventional-Commits commit subjects, a
|
|
222
|
+
`review: <artifact> (EP-<slug>)` title, and the hub artifact-review template body; **any other head is
|
|
223
|
+
a tooling/code change to the hub itself** and follows the `code` convention (a Conventional-Commits
|
|
224
|
+
title + the code task template), so a PR that changes the hub's own workflows/checks can pass.
|
|
225
|
+
`yad check --fix` installs the same `checks/*.sh` scripts plus a standalone hub workflow
|
|
208
226
|
(`templates/github/yad-hub-checks.yml` → `.github/workflows/yad-hub-checks.yml`, or the GitLab fragment
|
|
209
227
|
`templates/gitlab/yad-hub-checks.gitlab-ci.yml` → `.gitlab/ci/yad-hub-checks.yml` + its one include
|
|
210
228
|
line). Code repos run the same three with `--profile code` inside the main `yad-checks` workflow.
|
|
@@ -1,43 +1,64 @@
|
|
|
1
1
|
# yad-managed: yad-checks
|
|
2
|
-
# Pattern gates for the PRODUCT HUB
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
2
|
+
# Pattern gates for the PRODUCT HUB. They run with `--profile hub` and split by head branch:
|
|
3
|
+
# review/EP-* PRs are front-half artifact-review vehicles — a `review: <artifact> (EP-<slug>)` title
|
|
4
|
+
# and the hub artifact-review template body.
|
|
5
|
+
# every other PR is a tooling/code change to the hub itself — a Conventional-Commits title and the
|
|
6
|
+
# code task template body (same convention the code repos use).
|
|
7
|
+
# The head ref is passed via --head so the gate can tell the two apart. Commit subjects are always
|
|
8
|
+
# Conventional-Commits. Standalone workflow so it never collides with the code-repo yad-checks workflow.
|
|
6
9
|
name: yad-hub-checks
|
|
7
10
|
on:
|
|
8
11
|
pull_request:
|
|
12
|
+
# `edited` (beyond the opened/synchronize/reopened defaults) so a PR title/body correction
|
|
13
|
+
# re-runs the pattern gates without a close/reopen — e.g. fixing a body the pr-template gate held.
|
|
14
|
+
types: [opened, synchronize, reopened, edited]
|
|
9
15
|
branches: ["**"]
|
|
10
16
|
|
|
11
17
|
jobs:
|
|
18
|
+
# commit-message + ledger-guard read the commit range, not the PR title/body — skip them on a bare
|
|
19
|
+
# `edited` event (only pr-title/pr-template need to re-check a title/body fix).
|
|
12
20
|
commit-message:
|
|
13
21
|
runs-on: ubuntu-latest
|
|
22
|
+
if: github.event.action != 'edited'
|
|
14
23
|
steps:
|
|
15
24
|
- uses: actions/checkout@v4
|
|
16
25
|
with: { fetch-depth: 0 }
|
|
17
26
|
- run: bash checks/commit-message.sh --profile hub "origin/${{ github.base_ref }}"
|
|
18
27
|
|
|
19
28
|
# Pass the title via env (never interpolate untrusted ${{ }} into a run line — injection-safe).
|
|
29
|
+
# fetch-depth: 0 + the base ref give us the PR's changed paths, so the gate can reject an artifact
|
|
30
|
+
# change (epics/**) riding a non-review head past the front-half review with a plain code title.
|
|
20
31
|
pr-title:
|
|
21
32
|
runs-on: ubuntu-latest
|
|
22
33
|
env:
|
|
23
34
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
35
|
+
PR_HEAD: ${{ github.event.pull_request.head.ref }}
|
|
36
|
+
BASE_REF: ${{ github.base_ref }}
|
|
24
37
|
steps:
|
|
25
38
|
- uses: actions/checkout@v4
|
|
26
|
-
|
|
39
|
+
with: { fetch-depth: 0 }
|
|
40
|
+
- run: |
|
|
41
|
+
changed="$(mktemp)"; git diff --name-only "origin/${BASE_REF}...HEAD" > "$changed"
|
|
42
|
+
bash checks/pr-title.sh --profile hub --head "$PR_HEAD" --changed "$changed" "$PR_TITLE"
|
|
27
43
|
|
|
28
44
|
pr-template:
|
|
29
45
|
runs-on: ubuntu-latest
|
|
30
46
|
env:
|
|
31
47
|
PR_BODY: ${{ github.event.pull_request.body }}
|
|
48
|
+
PR_HEAD: ${{ github.event.pull_request.head.ref }}
|
|
49
|
+
BASE_REF: ${{ github.base_ref }}
|
|
32
50
|
steps:
|
|
33
51
|
- uses: actions/checkout@v4
|
|
52
|
+
with: { fetch-depth: 0 }
|
|
34
53
|
- run: |
|
|
54
|
+
changed="$(mktemp)"; git diff --name-only "origin/${BASE_REF}...HEAD" > "$changed"
|
|
35
55
|
body="$(mktemp)"; printf '%s' "$PR_BODY" > "$body"
|
|
36
|
-
bash checks/pr-template.sh --profile hub "$body"
|
|
56
|
+
bash checks/pr-template.sh --profile hub --head "$PR_HEAD" --changed "$changed" "$body"
|
|
37
57
|
|
|
38
58
|
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
|
|
39
59
|
ledger-guard:
|
|
40
60
|
runs-on: ubuntu-latest
|
|
61
|
+
if: github.event.action != 'edited'
|
|
41
62
|
steps:
|
|
42
63
|
- uses: actions/checkout@v4
|
|
43
64
|
with: { fetch-depth: 0 }
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
# .gitlab-ci.yml via:
|
|
4
4
|
# include:
|
|
5
5
|
# - local: '.gitlab/ci/yad-hub-checks.yml'
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# MRs run with `--profile hub` and split by source branch: review/EP-* MRs are front-half artifact-
|
|
7
|
+
# review vehicles (a `review: <artifact> (EP-<slug>)` title + the hub artifact-review template body);
|
|
8
|
+
# every other MR is a tooling/code change to the hub itself and follows the code convention (a
|
|
9
|
+
# Conventional-Commits title + the code task template body). The source branch is passed via --head so
|
|
10
|
+
# the gate can tell the two apart. Commit subjects are always Conventional-Commits.
|
|
9
11
|
# Job names are yad-hub-prefixed so they coexist with any code-repo fragment in one pipeline; jobs use
|
|
10
12
|
# `needs: []` and no `stage:` so a foreign `stages:` list can neither break nor reorder them.
|
|
11
13
|
default:
|
|
@@ -25,18 +27,22 @@ yad-hub-commit-message:
|
|
|
25
27
|
script:
|
|
26
28
|
- bash checks/commit-message.sh --profile hub "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
|
27
29
|
|
|
30
|
+
# GIT_DEPTH: 0 (above) gives the changed paths, so the gate can reject an artifact change (epics/**)
|
|
31
|
+
# riding a non-review source branch past the front-half review with a plain code title/template.
|
|
28
32
|
yad-hub-pr-title:
|
|
29
33
|
extends: .yad_hub_mr_only
|
|
30
34
|
needs: []
|
|
31
35
|
script:
|
|
32
|
-
-
|
|
36
|
+
- changed="$(mktemp)"; git diff --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD" > "$changed"
|
|
37
|
+
- bash checks/pr-title.sh --profile hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$CI_MERGE_REQUEST_TITLE"
|
|
33
38
|
|
|
34
39
|
yad-hub-pr-template:
|
|
35
40
|
extends: .yad_hub_mr_only
|
|
36
41
|
needs: []
|
|
37
42
|
script:
|
|
43
|
+
- changed="$(mktemp)"; git diff --name-only "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD" > "$changed"
|
|
38
44
|
- body="$(mktemp)"; printf '%s' "$CI_MERGE_REQUEST_DESCRIPTION" > "$body"
|
|
39
|
-
- bash checks/pr-template.sh --profile hub "$body"
|
|
45
|
+
- bash checks/pr-template.sh --profile hub --head "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --changed "$changed" "$body"
|
|
40
46
|
|
|
41
47
|
# The gate ledger is CI-owned: reject non-bot commits to .sdlc/*.json or reviews/*.md.
|
|
42
48
|
yad-hub-ledger-guard:
|
|
@@ -21,7 +21,8 @@ and `yad-ship` use. It **never auto-advances**; it just commits.
|
|
|
21
21
|
`feat|fix|docs|refactor|test|perf|build|ci|chore|revert`; proper nouns/acronyms keep their case.
|
|
22
22
|
- **Task trailer** — required on a code repo (anchors the `spec-link` + `commit-message` gates). Given
|
|
23
23
|
with `--task`, else derived from the branch (`feat/<story>-<task>-…`). Hub commits are not
|
|
24
|
-
task-scoped, so the trailer is optional there
|
|
24
|
+
task-scoped, so the trailer is optional there — a missing-Task warning is informational on the hub
|
|
25
|
+
(`spec-link` is a code-repo gate) and only flags a real gate failure in a code repo.
|
|
25
26
|
- **Contract-Change trailer** — `--contract-change` only when the diff touches the locked contract
|
|
26
27
|
surface; it routes the change back to the architecture gate.
|
|
27
28
|
- **AI co-author footer — OFF by default.** No `Co-Authored-By` trailer is written unless `--ai <id>`
|
|
@@ -22,6 +22,14 @@ the product hub.
|
|
|
22
22
|
`.gitlab/merge_request_templates/Default.md`) with `Task:`, `Risk level:`, `Contract surface
|
|
23
23
|
touched:`, and `Domains` prefilled; the rest is left for the author. This satisfies the `pr-template`
|
|
24
24
|
gate.
|
|
25
|
+
- **Stage-aware on the product hub** — `open-pr` mirrors the `--head` split the hub gates apply:
|
|
26
|
+
- a **`review/EP-*/<artifact>`** branch is a front-half artifact-review PR → it **delegates to
|
|
27
|
+
`yad gate open`** (artifact-review title `review: <artifact> (EP-<slug>)`, the hub artifact-review
|
|
28
|
+
body, and the gate ledger bookkeeping all in one place). Any `--title`/`-m` is ignored here.
|
|
29
|
+
- any **other hub branch** is a tooling/CI change → it uses the bundled **code-task** template
|
|
30
|
+
(`## Summary` / `Risk level:` / `## Checklist`) instead of the hub's artifact-review
|
|
31
|
+
`pull_request_template.md`, so the hub `pr-template` gate passes.
|
|
32
|
+
In a code repo nothing changes — it reads the repo's own committed code-task template.
|
|
25
33
|
- **Auto-assign** — from the hub roster scoped to this repo: assignee = the committer (resolved from
|
|
26
34
|
the local git identity), reviewers = the repo's `reviewer`/`domain-owner` logins minus the committer.
|
|
27
35
|
Degrades cleanly when there is no roster.
|
|
@@ -7,21 +7,38 @@
|
|
|
7
7
|
# requires `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled `Risk level:` (low|medium|high).
|
|
8
8
|
# --profile hub — the front-half artifact-review template (templates/hub/<platform>/):
|
|
9
9
|
# requires `## Artifact under review`, `## Impact & Risk (front-half)`, `## Checklist`, and a `Risk tags:` line.
|
|
10
|
+
# BUT only for review/EP-* head branches. Every other hub PR is a tooling/code change to the hub
|
|
11
|
+
# itself and uses the code task template instead; pass the head ref via --head so the gate knows
|
|
12
|
+
# which template to require. With no --head, the hub profile stays strict (artifact-review template).
|
|
13
|
+
# Branch name is not enough on its own: a non-review head that actually changes front-half
|
|
14
|
+
# artifacts (epics/**) would otherwise slip past the review workflow with only the code template.
|
|
15
|
+
# Pass the PR's changed paths via --changed <file> (one path per line); when they touch epics/**
|
|
16
|
+
# on a non-review head the gate FAILS — artifact changes must go through a review/EP-* PR.
|
|
10
17
|
# The body is passed as a FILE path (single positional arg); CI writes the event body to a temp file
|
|
11
18
|
# (GitHub: github.event.pull_request.body; GitLab: $CI_MERGE_REQUEST_DESCRIPTION).
|
|
12
19
|
set -euo pipefail
|
|
13
20
|
|
|
14
21
|
PROFILE=code
|
|
22
|
+
HEADREF=""
|
|
23
|
+
CHANGED=""
|
|
15
24
|
ARGS=()
|
|
16
25
|
while [ $# -gt 0 ]; do
|
|
17
26
|
case "$1" in
|
|
18
27
|
--profile) PROFILE="${2:-code}"; shift 2 ;;
|
|
19
28
|
--profile=*) PROFILE="${1#*=}"; shift ;;
|
|
29
|
+
--head) HEADREF="${2:-}"; shift 2 ;;
|
|
30
|
+
--head=*) HEADREF="${1#*=}"; shift ;;
|
|
31
|
+
--changed) CHANGED="${2:-}"; shift 2 ;;
|
|
32
|
+
--changed=*) CHANGED="${1#*=}"; shift ;;
|
|
20
33
|
*) ARGS+=("$1"); shift ;;
|
|
21
34
|
esac
|
|
22
35
|
done
|
|
23
36
|
case "$PROFILE" in code|hub) ;; *) echo "FAIL [pr-template]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
|
|
24
37
|
|
|
38
|
+
# True when the PR changes a front-half artifact (anything under epics/**). Reads the --changed list
|
|
39
|
+
# of paths CI computed from the PR diff; with no list (direct caller / test) it reports false.
|
|
40
|
+
artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && grep -qE '^epics/' "$CHANGED"; }
|
|
41
|
+
|
|
25
42
|
BODY="${ARGS[0]:-}"
|
|
26
43
|
if [ -z "$BODY" ] || [ ! -f "$BODY" ]; then
|
|
27
44
|
echo "FAIL [pr-template]: body file not found — pass the PR/MR description as a file path."
|
|
@@ -36,19 +53,14 @@ require_heading() {
|
|
|
36
53
|
fi
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
require_heading '## Checklist' '## Checklist'
|
|
43
|
-
if ! grep -qiE '(\*\*)?Risk tags:' "$BODY"; then
|
|
44
|
-
echo "FAIL [pr-template]: missing 'Risk tags:' line (front-half Impact & Risk)."
|
|
45
|
-
rc=1
|
|
46
|
-
fi
|
|
47
|
-
else
|
|
56
|
+
# The code task template: `## Summary`, `## Impact & Risk`, `## Checklist`, and a filled `Risk level:`.
|
|
57
|
+
# Used by the code profile and by hub tooling PRs (any head branch that is not review/EP-*).
|
|
58
|
+
check_code_body() {
|
|
48
59
|
require_heading '## Summary' '## Summary'
|
|
49
60
|
require_heading '## Impact & Risk' '## Impact & Risk'
|
|
50
61
|
require_heading '## Checklist' '## Checklist'
|
|
51
62
|
# Risk level must be present AND filled with a real value (not the <placeholder>).
|
|
63
|
+
local rl
|
|
52
64
|
rl="$(grep -iE '(\*\*)?Risk level:' "$BODY" | head -1 \
|
|
53
65
|
| sed -E 's/<!--.*$//; s/^[^:]*://; s/[*`]//g; s/^[[:space:]]*//; s/[[:space:]]*$//' || true)"
|
|
54
66
|
rl="$(printf '%s' "$rl" | tr 'A-Z' 'a-z' | grep -oE 'low|medium|high' | head -1 || true)"
|
|
@@ -56,7 +68,38 @@ else
|
|
|
56
68
|
echo "FAIL [pr-template]: 'Risk level:' missing or not set to low|medium|high."
|
|
57
69
|
rc=1
|
|
58
70
|
fi
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# The front-half artifact-review template.
|
|
74
|
+
check_hub_body() {
|
|
75
|
+
require_heading '## Artifact under review' '## Artifact under review'
|
|
76
|
+
require_heading '## Impact & Risk \(front-half\)' '## Impact & Risk (front-half)'
|
|
77
|
+
require_heading '## Checklist' '## Checklist'
|
|
78
|
+
if ! grep -qiE '(\*\*)?Risk tags:' "$BODY"; then
|
|
79
|
+
echo "FAIL [pr-template]: missing 'Risk tags:' line (front-half Impact & Risk)."
|
|
80
|
+
rc=1
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
KIND="$PROFILE"
|
|
85
|
+
if [ "$PROFILE" = hub ]; then
|
|
86
|
+
case "$HEADREF" in
|
|
87
|
+
review/EP-*|"") check_hub_body ;; # artifact-review PR (or unknown head — stay strict)
|
|
88
|
+
*)
|
|
89
|
+
# tooling/code change to the hub itself — UNLESS it changes front-half artifacts (epics/**),
|
|
90
|
+
# which must go through a review/EP-* PR. Without this guard a non-review head could carry an
|
|
91
|
+
# artifact change past the front-half review with only the code template.
|
|
92
|
+
if artifact_changed; then
|
|
93
|
+
echo "FAIL [pr-template]: head '${HEADREF}' changes front-half artifacts (epics/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
|
|
94
|
+
rc=1
|
|
95
|
+
else
|
|
96
|
+
check_code_body; KIND="hub-tooling"
|
|
97
|
+
fi
|
|
98
|
+
;;
|
|
99
|
+
esac
|
|
100
|
+
else
|
|
101
|
+
check_code_body
|
|
59
102
|
fi
|
|
60
103
|
|
|
61
|
-
[ "$rc" = 0 ] && echo "PASS [pr-template]: body uses the ${
|
|
104
|
+
[ "$rc" = 0 ] && echo "PASS [pr-template]: body uses the ${KIND} template (required sections present)."
|
|
62
105
|
exit "$rc"
|
|
@@ -5,22 +5,39 @@
|
|
|
5
5
|
# period (config.yaml build.pr_title_style: same_as_commit_subject; one task = one PR, the title is
|
|
6
6
|
# the squash-merge subject). Keep <type> in sync with cli/manifest.mjs COMMIT_TYPES.
|
|
7
7
|
# --profile hub — a front-half artifact-review title "review: <artifact> (EP-<slug>)", the shape
|
|
8
|
-
# `yad gate open` creates (cli/gate.mjs).
|
|
8
|
+
# `yad gate open` creates (cli/gate.mjs) — BUT only for review/EP-* head branches. Every other
|
|
9
|
+
# hub PR is a tooling/code change to the hub itself and follows the code convention; pass the
|
|
10
|
+
# head ref via --head so the gate can tell the two apart (a tooling PR has no EP artifact to
|
|
11
|
+
# review). With no --head, the hub profile stays strict (requires the review shape).
|
|
12
|
+
# Branch name is not enough on its own: a non-review head that actually changes front-half
|
|
13
|
+
# artifacts (epics/**) would otherwise slip past the review workflow with a plain code title.
|
|
14
|
+
# Pass the PR's changed paths via --changed <file> (one path per line); when they touch epics/**
|
|
15
|
+
# on a non-review head the gate FAILS — artifact changes must go through a review/EP-* PR.
|
|
9
16
|
# The title is passed as the (single) positional arg; CI injects it from the event payload
|
|
10
17
|
# (GitHub: github.event.pull_request.title; GitLab: $CI_MERGE_REQUEST_TITLE).
|
|
11
18
|
set -euo pipefail
|
|
12
19
|
|
|
13
20
|
PROFILE=code
|
|
21
|
+
HEADREF=""
|
|
22
|
+
CHANGED=""
|
|
14
23
|
ARGS=()
|
|
15
24
|
while [ $# -gt 0 ]; do
|
|
16
25
|
case "$1" in
|
|
17
26
|
--profile) PROFILE="${2:-code}"; shift 2 ;;
|
|
18
27
|
--profile=*) PROFILE="${1#*=}"; shift ;;
|
|
28
|
+
--head) HEADREF="${2:-}"; shift 2 ;;
|
|
29
|
+
--head=*) HEADREF="${1#*=}"; shift ;;
|
|
30
|
+
--changed) CHANGED="${2:-}"; shift 2 ;;
|
|
31
|
+
--changed=*) CHANGED="${1#*=}"; shift ;;
|
|
19
32
|
*) ARGS+=("$1"); shift ;;
|
|
20
33
|
esac
|
|
21
34
|
done
|
|
22
35
|
case "$PROFILE" in code|hub) ;; *) echo "FAIL [pr-title]: unknown --profile '$PROFILE' (code|hub)."; exit 1 ;; esac
|
|
23
36
|
|
|
37
|
+
# True when the PR changes a front-half artifact (anything under epics/**). Reads the --changed list
|
|
38
|
+
# of paths CI computed from the PR diff; with no list (direct caller / test) it reports false.
|
|
39
|
+
artifact_changed() { [ -n "$CHANGED" ] && [ -f "$CHANGED" ] && grep -qE '^epics/' "$CHANGED"; }
|
|
40
|
+
|
|
24
41
|
TITLE="${ARGS[0]:-}"
|
|
25
42
|
if [ -z "$TITLE" ]; then
|
|
26
43
|
echo "FAIL [pr-title]: empty title — pass the PR/MR title as the argument."
|
|
@@ -29,23 +46,43 @@ fi
|
|
|
29
46
|
|
|
30
47
|
TYPES='feat|fix|docs|refactor|test|perf|build|ci|chore|revert'
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
# Conventional-Commits subject (optional scope + breaking `!`), no trailing period. Used by the code
|
|
50
|
+
# profile and by hub tooling PRs (any head branch that is not review/EP-*).
|
|
51
|
+
check_code_title() {
|
|
52
|
+
if ! printf '%s' "$TITLE" | grep -qE "^(${TYPES})(\([a-z0-9._-]+\))?!?: .+"; then
|
|
53
|
+
echo "FAIL [pr-title]: '${TITLE}' is not '<type>(<scope>)?!?: <description>' (type one of: ${TYPES//|/, })."
|
|
54
|
+
exit 1
|
|
37
55
|
fi
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
56
|
+
if printf '%s' "$TITLE" | grep -qE '\.$'; then
|
|
57
|
+
echo "FAIL [pr-title]: '${TITLE}' must not end with a period."
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
echo "PASS [pr-title]: '${TITLE}' (profile: ${PROFILE}, tooling/code)"
|
|
61
|
+
exit 0
|
|
62
|
+
}
|
|
41
63
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
if [ "$PROFILE" = hub ]; then
|
|
65
|
+
# review/EP-* head branch (or unknown head ref) => front-half artifact-review PR: 'review: <artifact> (EP-<slug>)'.
|
|
66
|
+
case "$HEADREF" in
|
|
67
|
+
review/EP-*|"")
|
|
68
|
+
if printf '%s' "$TITLE" | grep -qE '^review: .+ \(EP-[a-z0-9-]+\)$'; then
|
|
69
|
+
echo "PASS [pr-title]: '${TITLE}' (profile: hub, artifact-review)"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
echo "FAIL [pr-title]: '${TITLE}' is not a hub review title 'review: <artifact> (EP-<slug>)'."
|
|
73
|
+
exit 1
|
|
74
|
+
;;
|
|
75
|
+
*)
|
|
76
|
+
# Any other hub PR is a tooling/code change to the hub itself — UNLESS it changes front-half
|
|
77
|
+
# artifacts (epics/**), which must go through a review/EP-* PR. Without this guard a non-review
|
|
78
|
+
# head could carry an artifact change past the front-half review with only a code title.
|
|
79
|
+
if artifact_changed; then
|
|
80
|
+
echo "FAIL [pr-title]: head '${HEADREF}' changes front-half artifacts (epics/**) but is not a review/EP-* branch — artifact changes must go through a review PR."
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
# tooling only — fall through to the code convention.
|
|
84
|
+
;;
|
|
85
|
+
esac
|
|
50
86
|
fi
|
|
51
|
-
|
|
87
|
+
|
|
88
|
+
check_code_title
|
package/skills/yad-ship/SKILL.md
CHANGED
|
@@ -22,6 +22,10 @@ its own and **never merges**. The engineer review + merge are Step E (`yad-engin
|
|
|
22
22
|
roster auto-assign, risk routing (`../yad-open-pr/SKILL.md`).
|
|
23
23
|
- **Order matters:** the PR/MR is opened **only if the commit lands**. A failed commit, a tripped
|
|
24
24
|
atomic guard, or `--dry-run` stops the step before anything is pushed.
|
|
25
|
+
- **Stage-aware on the product hub** (via `yad-open-pr`): on a `review/EP-*` branch `ship` opens the
|
|
26
|
+
front-half **artifact-review** PR (delegating to `yad gate open` — `--title` is ignored); on any
|
|
27
|
+
other hub branch it opens the **code-task** PR from the bundled code-task template. In a code repo
|
|
28
|
+
it is unchanged.
|
|
25
29
|
|
|
26
30
|
## Inputs
|
|
27
31
|
|