github-delivery-os 1.1.0 → 1.2.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/.claude/skills/delivery-ops/SKILL.md +172 -0
- package/.github/scripts/authorize-deployment-verdict.js +55 -0
- package/.github/scripts/auto-close-sprint.js +124 -0
- package/.github/scripts/sprint-child-creator.js +34 -0
- package/.github/workflows/authorize-deployment.yml +38 -13
- package/.github/workflows/auto-close-sprint.yml +13 -7
- package/.github/workflows/telegram-issues.yml +12 -6
- package/README.md +3 -1
- package/package.json +13 -3
- package/src/cli.js +4 -0
- package/src/install.js +267 -48
- package/.github/workflows/ci.yml +0 -23
- package/.github/workflows/pages.yml +0 -46
- package/.github/workflows/release.yml +0 -38
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delivery-ops
|
|
3
|
+
description: Operate a repo that has GitHub Delivery OS installed — create sprint/production-release/QA-request/bug issues that actually trigger its automation, comment as an approver in phrasing its workflows recognize, and check status (labels, latest comments, burn-down). Targets a specific repo via --repo; defaults to the current repo if this skill was installed into it and none is named. Use when asked to create a sprint, request a release, approve/decline a release, check release or sprint status, or demo/test Delivery OS against a given repo.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Operate Delivery OS
|
|
7
|
+
|
|
8
|
+
This drives the actual product — the workflows Delivery OS installs into a consumer repo — as a user of that repo would, not the tooling that ships the `github-delivery-os` package itself (that's the separate `release` skill). Use it to create issues that correctly trigger the installed automation, comment in a way the automation actually recognizes, and check what state something is in.
|
|
9
|
+
|
|
10
|
+
## Which repo?
|
|
11
|
+
|
|
12
|
+
Every command below takes `--repo <owner>/<name>` explicitly — never assume based on the working directory alone. Two ways this gets decided:
|
|
13
|
+
|
|
14
|
+
- **This skill was installed via `npx github-delivery-os install --with-skill`, into a specific repo's own `.claude/skills/`.** In that case the current working directory *is* the repo Delivery OS is installed in, so it's a reasonable default target if the user doesn't name a different one — just confirm that's what they mean before acting.
|
|
15
|
+
- **This skill is installed generically** (copied into `~/.claude/skills/`, available across every project). Here there's no natural default — the working directory could be anything. Ask which repo if it isn't named.
|
|
16
|
+
|
|
17
|
+
Creating issues and comments in a repo is a visible, outward action — other collaborators see it. Confirm the target repo and intent before creating anything real, the same as any other action that shows up in someone else's GitHub activity.
|
|
18
|
+
|
|
19
|
+
## Pre-flight check
|
|
20
|
+
|
|
21
|
+
Before creating anything that depends on configuration, check the target repo actually has Delivery OS installed and configured — a silent no-op (nothing happens because a variable is unset) is more confusing than an upfront "this won't do much yet":
|
|
22
|
+
|
|
23
|
+
- **Installed?** `gh api repos/<owner>/<repo>/contents/.github/workflows/authorize-deployment.yml --silent` (404 = not installed — suggest `npx github-delivery-os status .` or `install --with-templates` in that repo).
|
|
24
|
+
- **Labels set up?** `gh label list --repo <owner>/<repo>` — look for `production`, `qa`, `qa-request`, `sprint`, `sprint-active`, `planning`, `declined`, `ready-for-deploy`. Missing labels mean `Setup Labels` hasn't been run there yet — offer to fix it directly rather than just reporting the gap: `gh workflow run setup-labels.yml --repo <owner>/<repo>` (it's a `workflow_dispatch` trigger, so this actually creates them on the spot). Confirm with the user first since it's a real change to their repo.
|
|
25
|
+
- **Repo variables set?** `gh variable list --repo <owner>/<repo>` — look for `RELEASE_APPROVER`, `QA_APPROVER`, `QA_ASSIGNEES`. If unset, say so plainly: the issue will still get created, but `notify-release-approver` will ping the literal placeholder `release-approver`/`qa-approver`, not a real person. Setting these requires repo admin access (`gh variable set NAME --repo <owner>/<repo> --body <value>`) — don't set them without being asked to, since they name a real person as approver.
|
|
26
|
+
|
|
27
|
+
## Creating issues
|
|
28
|
+
|
|
29
|
+
`gh issue create` does not render GitHub's Issue Forms (the `.github/ISSUE_TEMPLATE/*.yml` files) — those only exist in the web UI. So the body has to be hand-built to match what a form submission would actually produce: `### <Field Label>` headings with the answer beneath each, because that heading text is exactly what the workflows regex-parse. Labels have to be passed explicitly too, since the template's auto-applied labels are also bypassed.
|
|
30
|
+
|
|
31
|
+
**Show the constructed title, body, and labels before actually creating the issue** — this is a real, visible action in someone else's repo, not a preview in this conversation. Get confirmation on the content, not just the target repo, before calling `gh issue create`.
|
|
32
|
+
|
|
33
|
+
**Sprint Planning** — triggers `sprint-child-creator` (one child issue per feature line, each labeled `sprint-active`, on open):
|
|
34
|
+
- Title **must contain** the literal string `SPRINT -`, e.g. `SPRINT - Sprint 14`
|
|
35
|
+
- Labels: `sprint`, `planning`
|
|
36
|
+
- Body:
|
|
37
|
+
```
|
|
38
|
+
### Sprint Name
|
|
39
|
+
|
|
40
|
+
<name>
|
|
41
|
+
|
|
42
|
+
### Sprint Start
|
|
43
|
+
|
|
44
|
+
YYYY-MM-DD
|
|
45
|
+
|
|
46
|
+
### Sprint End
|
|
47
|
+
|
|
48
|
+
YYYY-MM-DD
|
|
49
|
+
|
|
50
|
+
### Sprint Goal
|
|
51
|
+
|
|
52
|
+
<goal>
|
|
53
|
+
|
|
54
|
+
### Sprint Features (One Per Line)
|
|
55
|
+
|
|
56
|
+
<feature one>
|
|
57
|
+
<feature two>
|
|
58
|
+
<feature three>
|
|
59
|
+
|
|
60
|
+
### Sprint Approved
|
|
61
|
+
|
|
62
|
+
Pending
|
|
63
|
+
```
|
|
64
|
+
One feature per line, no bullets/numbering (matches the template's own instruction — `sprint-child-creator`'s parser just splits on newlines).
|
|
65
|
+
|
|
66
|
+
**Production Release** — triggers `notify-release-approver` on open (posts a comment tagging `RELEASE_APPROVER`), and later `authorize-deployment` on comments:
|
|
67
|
+
- Title: `PRODUCTION RELEASE - <project> - vX.X.X`
|
|
68
|
+
- Labels: `release`, `production`, `approval`
|
|
69
|
+
- Body:
|
|
70
|
+
```
|
|
71
|
+
### Sprint Reference (Sprint Planning Issue #)
|
|
72
|
+
|
|
73
|
+
#<N>
|
|
74
|
+
|
|
75
|
+
### Version / Build Number
|
|
76
|
+
|
|
77
|
+
vX.X.X
|
|
78
|
+
|
|
79
|
+
### Release Summary
|
|
80
|
+
|
|
81
|
+
<summary>
|
|
82
|
+
|
|
83
|
+
### QA Summary + Evidence Links
|
|
84
|
+
|
|
85
|
+
<links, or "None yet">
|
|
86
|
+
|
|
87
|
+
### Overall QA Recommendation
|
|
88
|
+
|
|
89
|
+
Approve for Production
|
|
90
|
+
|
|
91
|
+
### Deployment Authorized
|
|
92
|
+
|
|
93
|
+
No
|
|
94
|
+
```
|
|
95
|
+
(`qa_recommendation` drives the "QA Recommendation" line `notify-release-approver` puts in its comment — use `Approve for Production`, `Reject Release`, or `Conditional Approval` verbatim, those are the three strings it checks for.)
|
|
96
|
+
|
|
97
|
+
**QA Request** — triggers `auto-assign-qa` (assigns `QA_ASSIGNEES`) on open:
|
|
98
|
+
- Title: `QA REQUEST - <feature/issue>`
|
|
99
|
+
- Labels: `qa-request`
|
|
100
|
+
- Body:
|
|
101
|
+
```
|
|
102
|
+
### Related Sprint Task Issue (#)
|
|
103
|
+
|
|
104
|
+
#<N>
|
|
105
|
+
|
|
106
|
+
### What to Test
|
|
107
|
+
|
|
108
|
+
<what to test>
|
|
109
|
+
|
|
110
|
+
### Environment + Build Link
|
|
111
|
+
|
|
112
|
+
<build link>
|
|
113
|
+
|
|
114
|
+
### Acceptance Criteria
|
|
115
|
+
|
|
116
|
+
<criteria>
|
|
117
|
+
|
|
118
|
+
### QA Outcome
|
|
119
|
+
|
|
120
|
+
Pending
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Bug Report**:
|
|
124
|
+
- Title: `[BUG] <one-line summary>`
|
|
125
|
+
- Labels: `bug`, `qa`
|
|
126
|
+
- Body: mirror `bug_report.yml`'s fields (`Platform(s) Affected`, `Severity`, `Build / Version`, `Bug Summary`, `Steps to Reproduce`, `Expected Result`, `Actual Result`, `Test Environment`) as `### <label>` / answer pairs.
|
|
127
|
+
|
|
128
|
+
**Task** — no automation trigger, just structured tracking:
|
|
129
|
+
- Title: `TASK - <one-line summary>`
|
|
130
|
+
- Labels: `task`
|
|
131
|
+
- Body: mirror `task.yml`'s fields (`Task Summary`, `Description`, `Owner`, `Priority` — `P0 - Critical` / `P1 - High` / `P2 - Medium` / `P3 - Low`, `Status` — `Backlog` / `In Progress` / `Blocked` / `Ready for Review` / `Done`, `Acceptance Criteria`, `Artifacts / Links`) as `### <label>` / answer pairs.
|
|
132
|
+
|
|
133
|
+
## Commenting as an approver
|
|
134
|
+
|
|
135
|
+
`authorize-deployment` only registers a comment if **both** of these hold:
|
|
136
|
+
- It's posted by the exact GitHub login configured in the repo's `RELEASE_APPROVER` or `QA_APPROVER` variable. `gh issue comment` posts as whichever account `gh auth status` shows — if that's not the configured approver, the comment is just a comment, nothing fires.
|
|
137
|
+
- The comment **leads with** one of the recognized keywords (case-insensitive; anything after the keyword is fine, but the keyword itself has to be at the start):
|
|
138
|
+
- Release approve: `approved`, `approve`, `ok`, `go ahead`
|
|
139
|
+
- Release decline: `declined`, `rejected`, `reject`, `not approved`
|
|
140
|
+
- QA approve: `qa approved`, `approved`, `qa ok`, `looks good`
|
|
141
|
+
|
|
142
|
+
A later qualifying comment from the same approver overrides an earlier one — a decline can be superseded by a later approval once fixes land, and vice versa.
|
|
143
|
+
|
|
144
|
+
**Before posting, check that the authenticated login actually matches the approver you're commenting as** — `gh auth status` (or `gh api user --jq .login`) against the `RELEASE_APPROVER`/`QA_APPROVER` value from the pre-flight check. If they don't match, say so and stop: the comment would still post, look successful, and do nothing — a silent no-op that's easy to miss without this check, since `gh issue comment` succeeds either way.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
gh issue comment <number> --repo <owner>/<repo> --body "Approved, ship it"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Checking status
|
|
151
|
+
|
|
152
|
+
- **Latest comment(s):** `gh issue view <number> --repo <owner>/<repo> --comments`
|
|
153
|
+
- **Current labels:** `gh issue view <number> --repo <owner>/<repo> --json labels`
|
|
154
|
+
- **Sprint burn-down:** read the sprint (parent) issue's body — `gh issue view <sprint-number> --repo <owner>/<repo> --json body` — and look for the `## 🚦 Sprint Status` section `auto-close-sprint` maintains (progress %, time elapsed %, health emoji, burn-down bar). It only exists after at least one child issue has closed.
|
|
155
|
+
|
|
156
|
+
## Advancing a sprint
|
|
157
|
+
|
|
158
|
+
Closing a sprint task (child) issue is what actually moves the burn-down — creating the sprint only creates the children, nothing updates until they close:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
gh issue close <number> --repo <owner>/<repo>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`auto-close-sprint` fires on close, re-reads every `sprint-active` issue whose body contains `Parent Sprint: #<N>`, recomputes progress, and rewrites the sprint issue's `## 🚦 Sprint Status` section. At 100% it also closes the sprint issue itself and posts a completion comment. Re-check the sprint issue's body afterward to see the update — it happens as a side effect of closing the child, not as a response visible on the child issue itself.
|
|
165
|
+
|
|
166
|
+
## Finding things
|
|
167
|
+
|
|
168
|
+
When there's no issue number in hand yet:
|
|
169
|
+
- **Production releases awaiting a decision:** `gh issue list --repo <owner>/<repo> --label production --state open`
|
|
170
|
+
- **Active sprints:** `gh issue list --repo <owner>/<repo> --label sprint --state open` (title contains `SPRINT -`)
|
|
171
|
+
- **Open QA requests:** `gh issue list --repo <owner>/<repo> --label qa-request --state open`
|
|
172
|
+
- **A sprint's own children:** `gh issue list --repo <owner>/<repo> --label sprint-active --search "\"Parent Sprint: #<N>\" in:body"` — the exact-phrase quotes matter, otherwise the search matches "Parent", "Sprint", and the number as separate free-text terms instead of the literal phrase
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pure verdict-computation logic for authorize-deployment.yml, extracted out
|
|
4
|
+
// of the inline actions/github-script step so it can be unit tested directly
|
|
5
|
+
// (see test/authorize-deployment-verdict.test.js) instead of only verified
|
|
6
|
+
// by hand whenever it changes. Has no dependency on the `github`/`context`
|
|
7
|
+
// globals actions/github-script injects — everything it needs is passed in.
|
|
8
|
+
|
|
9
|
+
// Anchored to comment start + word boundary so keywords must lead the
|
|
10
|
+
// comment (matches the documented convention) and can't match inside a
|
|
11
|
+
// larger word (e.g. "ok" no longer matches "okay", "reject" no longer
|
|
12
|
+
// matches "rejection") or as a substring anywhere in unrelated prose.
|
|
13
|
+
const DECLINE_RE = /^(declined|rejected|reject|not approved)\b/i;
|
|
14
|
+
const APPROVE_RE = /^(approved|approve|ok|go ahead)\b/i;
|
|
15
|
+
const QA_APPROVE_RE = /^(qa approved|approved|qa ok|looks good)\b/i;
|
|
16
|
+
|
|
17
|
+
const normalize = (s) => (s || '').toLowerCase();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Walks comments in chronological order and keeps the LATEST verdict from
|
|
21
|
+
* each approver, rather than stopping at the first decline seen. This lets a
|
|
22
|
+
* release approver re-approve after an earlier decline (e.g. once fixes
|
|
23
|
+
* land) instead of being permanently stuck as declined.
|
|
24
|
+
*
|
|
25
|
+
* @param {Array<{ user?: { login?: string | null } | null, body?: string | null }>} comments
|
|
26
|
+
* Chronological (oldest first), matching the order github.paginate(listComments) returns.
|
|
27
|
+
* @param {string} releaseApprover
|
|
28
|
+
* @param {string} qaApprover
|
|
29
|
+
* @returns {{ releaseVerdict: 'approved' | 'declined' | null, qaApproved: boolean }}
|
|
30
|
+
*/
|
|
31
|
+
function computeVerdict(comments, releaseApprover, qaApprover) {
|
|
32
|
+
let releaseVerdict = null;
|
|
33
|
+
let qaApproved = false;
|
|
34
|
+
|
|
35
|
+
for (const comment of comments) {
|
|
36
|
+
const login = comment.user && comment.user.login;
|
|
37
|
+
const body = (comment.body || '').trim();
|
|
38
|
+
|
|
39
|
+
if (normalize(login) === normalize(releaseApprover)) {
|
|
40
|
+
if (DECLINE_RE.test(body)) {
|
|
41
|
+
releaseVerdict = 'declined';
|
|
42
|
+
} else if (APPROVE_RE.test(body)) {
|
|
43
|
+
releaseVerdict = 'approved';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (normalize(login) === normalize(qaApprover) && QA_APPROVE_RE.test(body)) {
|
|
48
|
+
qaApproved = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { releaseVerdict, qaApproved };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { computeVerdict, DECLINE_RE, APPROVE_RE, QA_APPROVE_RE };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pure burn-down/health computation for auto-close-sprint.yml, extracted so
|
|
4
|
+
// it can be unit tested directly (see test/auto-close-sprint.test.js)
|
|
5
|
+
// instead of only verified by hand.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} body - a child issue's body
|
|
9
|
+
* @returns {number | null} the parent sprint issue number, or null if this
|
|
10
|
+
* issue isn't a sprint child (no "Parent Sprint: #N" line).
|
|
11
|
+
*/
|
|
12
|
+
function parseParentSprintNumber(body) {
|
|
13
|
+
const match = (body || '').match(/Parent Sprint:\s*#(\d+)/);
|
|
14
|
+
return match ? parseInt(match[1], 10) : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses sprint dates out of a sprint issue's body, supporting both the
|
|
19
|
+
* combined "Sprint Dates: X to Y" format and the separate "Sprint Start" /
|
|
20
|
+
* "Sprint End" field format (the sprint_planning.yml template).
|
|
21
|
+
*
|
|
22
|
+
* @param {string} sprintBody - the sprint (parent) issue's body
|
|
23
|
+
* @returns {{ startDate: Date, endDate: Date } | null} null if neither
|
|
24
|
+
* format matched.
|
|
25
|
+
*/
|
|
26
|
+
function parseSprintDates(sprintBody) {
|
|
27
|
+
const body = sprintBody || '';
|
|
28
|
+
|
|
29
|
+
const rangeMatch = body.match(/Sprint Dates[\s\S]*?(\d{4}-\d{2}-\d{2})\s*to\s*(\d{4}-\d{2}-\d{2})/i);
|
|
30
|
+
if (rangeMatch) {
|
|
31
|
+
return { startDate: new Date(rangeMatch[1]), endDate: new Date(rangeMatch[2]) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const startMatch = body.match(/Sprint Start[\s\S]*?(\d{4}-\d{2}-\d{2})/i);
|
|
35
|
+
const endMatch = body.match(/Sprint End[\s\S]*?(\d{4}-\d{2}-\d{2})/i);
|
|
36
|
+
if (startMatch && endMatch) {
|
|
37
|
+
return { startDate: new Date(startMatch[1]), endDate: new Date(endMatch[1]) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Date} startDate
|
|
45
|
+
* @param {Date} endDate
|
|
46
|
+
* @param {Date} now
|
|
47
|
+
* @returns {number} percent of the sprint's duration elapsed as of `now`,
|
|
48
|
+
* clamped to [0, 100]. Returns 100 for a zero-or-negative-length sprint
|
|
49
|
+
* (Start >= End — a data-entry mistake) or an unparseable date (e.g. an
|
|
50
|
+
* out-of-range "2026-13-05" typo, which regex-matches the YYYY-MM-DD
|
|
51
|
+
* shape but produces an Invalid Date) instead of dividing by zero or NaN:
|
|
52
|
+
* that NaN would otherwise flow into the posted burn-down as "Time
|
|
53
|
+
* Elapsed: NaN%", and every NaN comparison in computeHealthEmoji is
|
|
54
|
+
* false, so it'd silently default to the green/no-warning branch instead
|
|
55
|
+
* of flagging the broken dates. `!(totalDuration > 0)` (rather than
|
|
56
|
+
* `totalDuration <= 0`) catches NaN too, since every comparison against
|
|
57
|
+
* NaN is false — `NaN <= 0` is false, but so is `NaN > 0`, and negating
|
|
58
|
+
* that is true.
|
|
59
|
+
*/
|
|
60
|
+
function computeTimePercent(startDate, endDate, now) {
|
|
61
|
+
const totalDuration = endDate - startDate;
|
|
62
|
+
if (!(totalDuration > 0)) return 100;
|
|
63
|
+
const elapsed = now - startDate;
|
|
64
|
+
return Math.max(0, Math.min(100, Math.round((elapsed / totalDuration) * 100)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {Array<{ state: string }>} children
|
|
69
|
+
* @returns {{ progressPercent: number, closedCount: number, totalCount: number }}
|
|
70
|
+
*/
|
|
71
|
+
function computeProgress(children) {
|
|
72
|
+
const totalCount = children.length;
|
|
73
|
+
const closedCount = children.filter((i) => i.state === 'closed').length;
|
|
74
|
+
const progressPercent = totalCount === 0 ? 0 : Math.round((closedCount / totalCount) * 100);
|
|
75
|
+
return { progressPercent, closedCount, totalCount };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sprint health relative to time elapsed: red if progress trails time by
|
|
80
|
+
* more than 10 points, yellow if behind at all, green otherwise.
|
|
81
|
+
*
|
|
82
|
+
* @returns {'🟢' | '🟡' | '🔴'}
|
|
83
|
+
*/
|
|
84
|
+
function computeHealthEmoji(progressPercent, timePercent) {
|
|
85
|
+
if (progressPercent < timePercent - 10) return '🔴';
|
|
86
|
+
if (progressPercent < timePercent) return '🟡';
|
|
87
|
+
return '🟢';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @returns {string} a block-character progress bar, e.g. "████░░░░░░░░░░░░░░░░".
|
|
92
|
+
*/
|
|
93
|
+
function renderBurnDown(progressPercent, totalBars = 20) {
|
|
94
|
+
const filledBars = Math.round((progressPercent / 100) * totalBars);
|
|
95
|
+
return '█'.repeat(filledBars) + '░'.repeat(totalBars - filledBars);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Replaces any previous "## 🚦 Sprint Status" block in the sprint body with
|
|
100
|
+
* a freshly rendered one (appended at the end).
|
|
101
|
+
*/
|
|
102
|
+
function updateSprintBody(sprintBody, { progressPercent, timePercent, healthEmoji, burnDown }) {
|
|
103
|
+
let updatedBody = (sprintBody || '').replace(/## 🚦 Sprint Status[\s\S]*?---/g, '');
|
|
104
|
+
|
|
105
|
+
updatedBody +=
|
|
106
|
+
`\n\n---\n\n## 🚦 Sprint Status\n\n` +
|
|
107
|
+
`Progress: **${progressPercent}%**\n` +
|
|
108
|
+
`Time Elapsed: **${timePercent}%**\n\n` +
|
|
109
|
+
`Health: ${healthEmoji}\n\n` +
|
|
110
|
+
`### 📉 Burn-down\n` +
|
|
111
|
+
`\`${burnDown}\` ${progressPercent}%\n\n---`;
|
|
112
|
+
|
|
113
|
+
return updatedBody;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
parseParentSprintNumber,
|
|
118
|
+
parseSprintDates,
|
|
119
|
+
computeTimePercent,
|
|
120
|
+
computeProgress,
|
|
121
|
+
computeHealthEmoji,
|
|
122
|
+
renderBurnDown,
|
|
123
|
+
updateSprintBody,
|
|
124
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pure parsing logic for sprint-child-creator.yml, extracted so it can be
|
|
4
|
+
// unit tested directly (see test/sprint-child-creator.test.js) instead of
|
|
5
|
+
// only verified by hand.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the feature list from a sprint-planning issue body's
|
|
9
|
+
* "### Sprint Features" section (one feature per line, up to the next
|
|
10
|
+
* heading or end of body).
|
|
11
|
+
*
|
|
12
|
+
* @param {string} body - the sprint issue's body
|
|
13
|
+
* @returns {string[]} feature titles, in the order they appear. Empty if
|
|
14
|
+
* there's no Sprint Features section, or it has no non-blank lines.
|
|
15
|
+
*/
|
|
16
|
+
function parseFeatures(body) {
|
|
17
|
+
const featuresMatch = (body || '').match(/### Sprint Features[\s\S]*?(?=###|$)/);
|
|
18
|
+
if (!featuresMatch) return [];
|
|
19
|
+
|
|
20
|
+
return featuresMatch[0]
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map((line) => line.trim())
|
|
23
|
+
.filter((line) => line && !line.startsWith('###'));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {number} parentNumber - the sprint (parent) issue number
|
|
28
|
+
* @returns {string} body to use for each generated child issue
|
|
29
|
+
*/
|
|
30
|
+
function buildChildBody(parentNumber) {
|
|
31
|
+
return `Parent Sprint: #${parentNumber}\n\n---\n*Created by Delivery OS Sprint Child Creator*`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { parseFeatures, buildChildBody };
|
|
@@ -58,19 +58,40 @@ jobs:
|
|
|
58
58
|
|
|
59
59
|
const { releaseVerdict, qaApproved } = computeVerdict(comments, releaseApprover, qaApprover);
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
// Nothing to act on this run (most comments on a production issue
|
|
62
|
+
// are routine discussion, not a verdict-changing approval/decline)
|
|
63
|
+
// — return before the live label fetch below, which only matters
|
|
64
|
+
// once we actually have a verdict to check against current state.
|
|
65
|
+
const hasActionableVerdict =
|
|
66
|
+
releaseVerdict === "declined" || (releaseVerdict === "approved" && qaApproved);
|
|
67
|
+
if (!hasActionableVerdict) return;
|
|
68
|
+
|
|
69
|
+
// Fetch labels live rather than trusting context.payload.issue.labels:
|
|
70
|
+
// that's a snapshot fixed at the triggering event's delivery time.
|
|
71
|
+
// Two comments posted seconds apart queue under the concurrency
|
|
72
|
+
// group above and run sequentially, but each was still delivered
|
|
73
|
+
// (and its label snapshot captured) before either job actually ran
|
|
74
|
+
// — serializing execution order doesn't refresh an already-stale
|
|
75
|
+
// snapshot. A live re-fetch is what actually lets the second
|
|
76
|
+
// (queued) run see what the first one just did.
|
|
77
|
+
const { data: liveIssue } = await github.rest.issues.get({ owner, repo, issue_number: issueNumber });
|
|
78
|
+
const currentLabels = (liveIssue.labels || []).map((l) => (typeof l === "string" ? l : l.name));
|
|
62
79
|
const alreadyDeclined = currentLabels.includes("declined");
|
|
63
80
|
const alreadyReady = currentLabels.includes("ready-for-deploy");
|
|
64
81
|
|
|
65
82
|
if (releaseVerdict === "declined") {
|
|
66
83
|
if (alreadyDeclined) return; // avoid re-posting on every later comment
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
try {
|
|
86
|
+
await github.rest.issues.addLabels({
|
|
87
|
+
owner,
|
|
88
|
+
repo,
|
|
89
|
+
issue_number: issueNumber,
|
|
90
|
+
labels: ["declined"]
|
|
91
|
+
});
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.log(`Failed to add 'declined' label: ${e.message || e}`);
|
|
94
|
+
}
|
|
74
95
|
|
|
75
96
|
try {
|
|
76
97
|
await github.rest.issues.removeLabel({
|
|
@@ -103,12 +124,16 @@ jobs:
|
|
|
103
124
|
});
|
|
104
125
|
} catch (e) {}
|
|
105
126
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
try {
|
|
128
|
+
await github.rest.issues.addLabels({
|
|
129
|
+
owner,
|
|
130
|
+
repo,
|
|
131
|
+
issue_number: issueNumber,
|
|
132
|
+
labels: ["ready-for-deploy"]
|
|
133
|
+
});
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.log(`Failed to add 'ready-for-deploy' label: ${e.message || e}`);
|
|
136
|
+
}
|
|
112
137
|
|
|
113
138
|
await github.rest.issues.createComment({
|
|
114
139
|
owner,
|
|
@@ -70,17 +70,23 @@ jobs:
|
|
|
70
70
|
|
|
71
71
|
// Paginate: a plain listForRepo call caps at one page (100 issues) and
|
|
72
72
|
// would silently undercount children in repos with more open+closed
|
|
73
|
-
// issues than that, skewing the burn-down.
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
73
|
+
// issues than that, skewing the burn-down.
|
|
74
|
+
//
|
|
75
|
+
// Deliberately NOT scoped to labels: "sprint-active" (an earlier
|
|
76
|
+
// version of this workflow did that, for cost — cheaper in repos
|
|
77
|
+
// with a lot of unrelated issue history). That's wrong: a child
|
|
78
|
+
// issue relabeled away from sprint-active during triage (e.g. to
|
|
79
|
+
// "bug") while still open would silently drop out of BOTH the
|
|
80
|
+
// numerator and denominator of the progress calculation instead
|
|
81
|
+
// of just being excluded from the closed count — the sprint could
|
|
82
|
+
// report inflated progress, or even reach a false 100% and
|
|
83
|
+
// auto-close, while that untracked child is still open. Matches
|
|
84
|
+
// docs/governance.md's documented trigger ("body has Parent
|
|
85
|
+
// Sprint"), which is body-content-based, not label-based.
|
|
79
86
|
const allIssues = await github.paginate(github.rest.issues.listForRepo, {
|
|
80
87
|
owner: context.repo.owner,
|
|
81
88
|
repo: context.repo.repo,
|
|
82
89
|
state: "all",
|
|
83
|
-
labels: "sprint-active",
|
|
84
90
|
per_page: 100
|
|
85
91
|
});
|
|
86
92
|
|
|
@@ -103,14 +103,20 @@ jobs:
|
|
|
103
103
|
# Case-insensitive actor comparison: GitHub logins are case-insensitive,
|
|
104
104
|
# but a plain bash == is not, so a RELEASE_APPROVER var with different
|
|
105
105
|
# casing than the actual login would otherwise never match here.
|
|
106
|
-
# Anchored to the start of a line
|
|
107
|
-
# convention as authorize-deployment.yml's
|
|
108
|
-
# can't fire for a comment that
|
|
109
|
-
# decline (e.g. "not
|
|
110
|
-
|
|
106
|
+
# Anchored to the start of a line + word boundary (\b), matching the
|
|
107
|
+
# same keyword set and convention as authorize-deployment.yml's real
|
|
108
|
+
# DECLINE_RE/APPROVE_RE, so this alert can't fire for a comment that
|
|
109
|
+
# workflow wouldn't actually treat as a decline/approval (e.g. "not
|
|
110
|
+
# approved" mid-sentence, or "okay, I'll look at this tomorrow" —
|
|
111
|
+
# "ok" without \b would otherwise match as a false approval).
|
|
112
|
+
if [[ "${ACTOR,,}" == "${RELEASE_APPROVER,,}" ]] && echo "$COMMENT_BODY" | grep -iqE "^(declined|rejected|reject|not approved)\b"; then
|
|
111
113
|
MESSAGE="🔴🛑 RELEASE DECLINED%0A$TITLE%0A$URL%0A---%0A👤 $ACTOR%0A🕒 $TIMESTAMP"
|
|
112
114
|
|
|
113
|
-
|
|
115
|
+
# Matches authorize-deployment.yml's full APPROVE_RE keyword set
|
|
116
|
+
# (approved|approve|ok|go ahead), not just "approved" — it used to
|
|
117
|
+
# miss "ok"/"go ahead"/"approve", so a release could be approved
|
|
118
|
+
# for real with no Telegram alert sent for it.
|
|
119
|
+
elif [[ "${ACTOR,,}" == "${RELEASE_APPROVER,,}" ]] && echo "$COMMENT_BODY" | grep -iqE "^(approved|approve|ok|go ahead)\b"; then
|
|
114
120
|
MESSAGE="🟢🛡️ RELEASE APPROVED%0A$TITLE%0A$URL%0A---%0A👤 $ACTOR%0A🕒 $TIMESTAMP"
|
|
115
121
|
|
|
116
122
|
elif [[ "${{ contains(github.event.issue.labels.*.name || fromJSON('[]'), 'bug') }}" == "true" ]]; then
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The **GitHub Delivery Operating System (Delivery OS)** embeds structured intake,
|
|
|
30
30
|
npx github-delivery-os install --with-templates .
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
From your repo root. Add `--with-labels` to create labels via `gh` CLI (requires `gh auth`). Use `--dry-run` to preview first.
|
|
33
|
+
From your repo root. Add `--with-labels` to create labels via `gh` CLI (requires `gh auth`). Add `--with-skill` to also drop in a [Claude Code](https://claude.com/claude-code) skill for operating this repo's Delivery OS from Claude Code — creating sprint/release/QA issues, commenting as an approver, checking status. Use `--dry-run` to preview first.
|
|
34
34
|
|
|
35
35
|
**Alternative — clone and run script:**
|
|
36
36
|
|
|
@@ -77,6 +77,8 @@ Workflows and templates are **copied directly** into your repo. No `workflow_cal
|
|
|
77
77
|
|
|
78
78
|
Installing via `npx github-delivery-os` (not the `scripts/install.sh` clone path) also writes `.github/delivery-os.json`, a small manifest recording the installed version — this is what powers the update check in `status`. It's only written when the files it describes are actually current (a fresh install, or `--overwrite`); a skip-mode install over existing files leaves it untouched rather than claiming a version that isn't really on disk. `uninstall` removes it.
|
|
79
79
|
|
|
80
|
+
With `--with-skill`, a `.claude/skills/delivery-ops/SKILL.md` file is also written — a Claude Code skill scoped to this repo, so anyone working here with Claude Code can create issues that correctly trigger the workflows above, comment as an approver, and check status without knowing the underlying `gh` commands or issue-body formats by heart. It's opt-in and retroactive: `--with-skill` on any later `install` call adds it if it isn't there yet.
|
|
81
|
+
|
|
80
82
|
---
|
|
81
83
|
|
|
82
84
|
## Quick Start (After Install)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-delivery-os",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "A GitHub-native Delivery Governance Framework for structured sprint execution, QA review, and collaborative production release control.",
|
|
5
5
|
"main": "src/install.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,8 +38,18 @@
|
|
|
38
38
|
"files": [
|
|
39
39
|
"bin",
|
|
40
40
|
"src",
|
|
41
|
-
".github/workflows",
|
|
42
|
-
".github/
|
|
41
|
+
".github/workflows/sprint-child-creator.yml",
|
|
42
|
+
".github/workflows/auto-close-sprint.yml",
|
|
43
|
+
".github/workflows/notify-release-approver.yml",
|
|
44
|
+
".github/workflows/authorize-deployment.yml",
|
|
45
|
+
".github/workflows/auto-assign-qa.yml",
|
|
46
|
+
".github/workflows/telegram-issues.yml",
|
|
47
|
+
".github/workflows/setup-labels.yml",
|
|
48
|
+
".github/scripts/authorize-deployment-verdict.js",
|
|
49
|
+
".github/scripts/auto-close-sprint.js",
|
|
50
|
+
".github/scripts/sprint-child-creator.js",
|
|
51
|
+
".github/ISSUE_TEMPLATE",
|
|
52
|
+
".claude/skills/delivery-ops"
|
|
43
53
|
],
|
|
44
54
|
"engines": {
|
|
45
55
|
"node": ">=14.0.0"
|
package/src/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ program
|
|
|
20
20
|
.description('Install workflows and templates into a repository')
|
|
21
21
|
.option('-t, --with-templates', 'Copy issue templates (sprint, task, bug, QA, production release)')
|
|
22
22
|
.option('-l, --with-labels', 'Create labels via gh CLI (requires gh auth)')
|
|
23
|
+
.option('-s, --with-skill', 'Add the delivery-ops Claude Code skill (.claude/skills/delivery-ops/SKILL.md)')
|
|
23
24
|
.option('-o, --overwrite', 'Replace existing workflow/template files')
|
|
24
25
|
.option('--no-overwrite', 'Skip existing files (default)')
|
|
25
26
|
.option('-d, --dry-run', 'Show what would happen without changing files')
|
|
@@ -29,6 +30,7 @@ program
|
|
|
29
30
|
targetDir,
|
|
30
31
|
withTemplates: options.withTemplates ?? false,
|
|
31
32
|
withLabels: options.withLabels ?? false,
|
|
33
|
+
withSkill: options.withSkill ?? false,
|
|
32
34
|
overwrite: options.overwrite ?? false,
|
|
33
35
|
dryRun: options.dryRun ?? false,
|
|
34
36
|
});
|
|
@@ -46,11 +48,13 @@ program
|
|
|
46
48
|
.command('uninstall [target]')
|
|
47
49
|
.description('Remove Delivery OS workflows (and optionally templates)')
|
|
48
50
|
.option('-t, --with-templates', 'Also remove issue templates')
|
|
51
|
+
.option('-s, --with-skill', 'Also remove the delivery-ops Claude Code skill')
|
|
49
52
|
.option('-d, --dry-run', 'Show what would be removed without deleting')
|
|
50
53
|
.action((target, options) => {
|
|
51
54
|
runUninstall({
|
|
52
55
|
targetDir: target || '.',
|
|
53
56
|
withTemplates: options.withTemplates ?? false,
|
|
57
|
+
withSkill: options.withSkill ?? false,
|
|
54
58
|
dryRun: options.dryRun ?? false,
|
|
55
59
|
});
|
|
56
60
|
});
|
package/src/install.js
CHANGED
|
@@ -4,7 +4,12 @@ const https = require('https');
|
|
|
4
4
|
const { execFileSync } = require('child_process');
|
|
5
5
|
|
|
6
6
|
const MANIFEST_FILE = 'delivery-os.json'; // written to .github/delivery-os.json in the target repo
|
|
7
|
+
const SKILL_REL_PATH = path.join('.claude', 'skills', 'delivery-ops', 'SKILL.md'); // opt-in via --with-skill
|
|
7
8
|
|
|
9
|
+
// If you add/remove/rename an entry here, also update package.json's "files"
|
|
10
|
+
// array — it lists these paths explicitly (not the whole .github/workflows
|
|
11
|
+
// directory) so this package's own maintainer workflows (ci.yml, release.yml,
|
|
12
|
+
// pages.yml) don't get bundled into what ships to consumers.
|
|
8
13
|
const WORKFLOWS = [
|
|
9
14
|
'sprint-child-creator',
|
|
10
15
|
'auto-close-sprint',
|
|
@@ -15,6 +20,22 @@ const WORKFLOWS = [
|
|
|
15
20
|
'setup-labels',
|
|
16
21
|
];
|
|
17
22
|
|
|
23
|
+
// Pure logic some of the workflows above require() at runtime from
|
|
24
|
+
// .github/scripts/<name>.js (see .github/workflows/authorize-deployment.yml
|
|
25
|
+
// etc.) — these are required dependencies of those workflows, not optional,
|
|
26
|
+
// so they're always copied alongside them, the same as WORKFLOWS. Also list
|
|
27
|
+
// them explicitly in package.json's "files".
|
|
28
|
+
const SCRIPTS = ['authorize-deployment-verdict', 'auto-close-sprint', 'sprint-child-creator'];
|
|
29
|
+
|
|
30
|
+
// Which workflow requires which script, so `status` can flag a workflow
|
|
31
|
+
// that's present but whose required script is missing (an install that will
|
|
32
|
+
// fail with MODULE_NOT_FOUND the next time that workflow actually runs).
|
|
33
|
+
const REQUIRED_SCRIPT_BY_WORKFLOW = {
|
|
34
|
+
'authorize-deployment': 'authorize-deployment-verdict',
|
|
35
|
+
'auto-close-sprint': 'auto-close-sprint',
|
|
36
|
+
'sprint-child-creator': 'sprint-child-creator',
|
|
37
|
+
};
|
|
38
|
+
|
|
18
39
|
const LABELS = [
|
|
19
40
|
['intake', '0E8A16'],
|
|
20
41
|
['bug', 'D93F0B'],
|
|
@@ -37,6 +58,10 @@ function manifestPath(targetAbs) {
|
|
|
37
58
|
return path.join(targetAbs, '.github', MANIFEST_FILE);
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
function skillPath(targetAbs) {
|
|
62
|
+
return path.join(targetAbs, SKILL_REL_PATH);
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
function readManifest(targetAbs) {
|
|
41
66
|
try {
|
|
42
67
|
return JSON.parse(fs.readFileSync(manifestPath(targetAbs), 'utf8'));
|
|
@@ -92,6 +117,44 @@ function fetchLatestVersion(timeoutMs = 3000) {
|
|
|
92
117
|
});
|
|
93
118
|
}
|
|
94
119
|
|
|
120
|
+
// Copies each `${name}${ext}` from srcDir to destDir for a fixed list of
|
|
121
|
+
// expected filenames — the shared logic behind copying WORKFLOWS and SCRIPTS
|
|
122
|
+
// (both: a required, always-on set of individually-named files, as opposed
|
|
123
|
+
// to templates, which copies whatever's found in a directory, or the skill,
|
|
124
|
+
// a single optional file). `label` is what's printed for each entry, e.g.
|
|
125
|
+
// `.github/scripts/auto-close-sprint.js` — pass names already including
|
|
126
|
+
// their directory prefix so log lines are self-explanatory on their own.
|
|
127
|
+
function copyManagedFiles(names, ext, srcDir, destDir, { overwrite, dryRun, relDir }) {
|
|
128
|
+
let copied = 0;
|
|
129
|
+
let skipped = 0;
|
|
130
|
+
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
const src = path.join(srcDir, `${name}${ext}`);
|
|
133
|
+
const dest = path.join(destDir, `${name}${ext}`);
|
|
134
|
+
const label = `${relDir}/${name}${ext}`;
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(src)) {
|
|
137
|
+
console.log(` Warning: source not found: ${label}`);
|
|
138
|
+
skipped++; // missing source must block a "clean install" claim, not just warn
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (fs.existsSync(dest) && !overwrite) {
|
|
143
|
+
console.log(` Skipped (exists): ${label}`);
|
|
144
|
+
skipped++;
|
|
145
|
+
} else if (dryRun) {
|
|
146
|
+
console.log(` [dry-run] Would create: ${label}`);
|
|
147
|
+
copied++;
|
|
148
|
+
} else {
|
|
149
|
+
fs.copyFileSync(src, dest);
|
|
150
|
+
console.log(` Created: ${label}`);
|
|
151
|
+
copied++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { copied, skipped };
|
|
156
|
+
}
|
|
157
|
+
|
|
95
158
|
function getPackageRoot() {
|
|
96
159
|
// When installed via npm, __dirname is node_modules/github-delivery-os/src
|
|
97
160
|
const possibleRoots = [
|
|
@@ -112,6 +175,7 @@ function runInstall(options) {
|
|
|
112
175
|
targetDir = '.',
|
|
113
176
|
withTemplates = false,
|
|
114
177
|
withLabels = false,
|
|
178
|
+
withSkill = false,
|
|
115
179
|
overwrite = false,
|
|
116
180
|
dryRun = false,
|
|
117
181
|
} = options;
|
|
@@ -119,6 +183,8 @@ function runInstall(options) {
|
|
|
119
183
|
const pkgRoot = getPackageRoot();
|
|
120
184
|
const workflowsSrc = path.join(pkgRoot, '.github', 'workflows');
|
|
121
185
|
const templatesSrc = path.join(pkgRoot, '.github', 'ISSUE_TEMPLATE');
|
|
186
|
+
const scriptsSrc = path.join(pkgRoot, '.github', 'scripts');
|
|
187
|
+
const skillSrc = path.join(pkgRoot, SKILL_REL_PATH);
|
|
122
188
|
const targetAbs = path.resolve(process.cwd(), targetDir);
|
|
123
189
|
|
|
124
190
|
console.log('=== GitHub Delivery Operating System ===');
|
|
@@ -140,39 +206,42 @@ function runInstall(options) {
|
|
|
140
206
|
// Ensure target structure
|
|
141
207
|
const workflowsDest = path.join(targetAbs, '.github', 'workflows');
|
|
142
208
|
const templatesDest = path.join(targetAbs, '.github', 'ISSUE_TEMPLATE');
|
|
209
|
+
const scriptsDest = path.join(targetAbs, '.github', 'scripts');
|
|
143
210
|
|
|
144
211
|
if (!dryRun) {
|
|
145
212
|
fs.mkdirSync(workflowsDest, { recursive: true });
|
|
146
213
|
fs.mkdirSync(templatesDest, { recursive: true });
|
|
214
|
+
fs.mkdirSync(scriptsDest, { recursive: true });
|
|
147
215
|
}
|
|
148
216
|
|
|
149
217
|
let workflowsCopied = 0;
|
|
150
218
|
let templatesCopied = 0;
|
|
151
|
-
let
|
|
152
|
-
let
|
|
219
|
+
let scriptsCopied = 0;
|
|
220
|
+
let skillCopied = 0;
|
|
221
|
+
let workflowsSkipped;
|
|
222
|
+
let scriptsSkipped;
|
|
223
|
+
|
|
224
|
+
// Copy workflows (always on — not optional)
|
|
225
|
+
({ copied: workflowsCopied, skipped: workflowsSkipped } = copyManagedFiles(
|
|
226
|
+
WORKFLOWS,
|
|
227
|
+
'.yml',
|
|
228
|
+
workflowsSrc,
|
|
229
|
+
workflowsDest,
|
|
230
|
+
{ overwrite, dryRun, relDir: '.github/workflows' }
|
|
231
|
+
));
|
|
232
|
+
|
|
233
|
+
// Copy the scripts the workflows above require() at runtime — required,
|
|
234
|
+
// not optional, so (unlike templates/skill) this always runs too.
|
|
235
|
+
({ copied: scriptsCopied, skipped: scriptsSkipped } = copyManagedFiles(
|
|
236
|
+
SCRIPTS,
|
|
237
|
+
'.js',
|
|
238
|
+
scriptsSrc,
|
|
239
|
+
scriptsDest,
|
|
240
|
+
{ overwrite, dryRun, relDir: '.github/scripts' }
|
|
241
|
+
));
|
|
153
242
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const src = path.join(workflowsSrc, `${wf}.yml`);
|
|
157
|
-
const dest = path.join(workflowsDest, `${wf}.yml`);
|
|
158
|
-
|
|
159
|
-
if (!fs.existsSync(src)) {
|
|
160
|
-
console.log(` Warning: source not found: ${wf}.yml`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (fs.existsSync(dest) && !overwrite) {
|
|
165
|
-
console.log(` Skipped (exists): ${wf}.yml`);
|
|
166
|
-
workflowsSkipped++;
|
|
167
|
-
} else if (dryRun) {
|
|
168
|
-
console.log(` [dry-run] Would create: ${wf}.yml`);
|
|
169
|
-
workflowsCopied++;
|
|
170
|
-
} else {
|
|
171
|
-
fs.copyFileSync(src, dest);
|
|
172
|
-
console.log(` Created: ${wf}.yml`);
|
|
173
|
-
workflowsCopied++;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
243
|
+
let templatesSkipped = 0;
|
|
244
|
+
let skillSkipped = 0;
|
|
176
245
|
|
|
177
246
|
// Copy templates
|
|
178
247
|
if (withTemplates && fs.existsSync(templatesSrc)) {
|
|
@@ -197,6 +266,25 @@ function runInstall(options) {
|
|
|
197
266
|
}
|
|
198
267
|
}
|
|
199
268
|
|
|
269
|
+
// Copy the delivery-ops Claude Code skill (opt-in — most consumer repos
|
|
270
|
+
// aren't using Claude Code, so this is never written unless asked for)
|
|
271
|
+
if (withSkill && fs.existsSync(skillSrc)) {
|
|
272
|
+
const skillDest = skillPath(targetAbs);
|
|
273
|
+
|
|
274
|
+
if (fs.existsSync(skillDest) && !overwrite) {
|
|
275
|
+
console.log(` Skipped (exists): ${SKILL_REL_PATH}`);
|
|
276
|
+
skillSkipped++;
|
|
277
|
+
} else if (dryRun) {
|
|
278
|
+
console.log(` [dry-run] Would create: ${SKILL_REL_PATH}`);
|
|
279
|
+
skillCopied++;
|
|
280
|
+
} else {
|
|
281
|
+
fs.mkdirSync(path.dirname(skillDest), { recursive: true });
|
|
282
|
+
fs.copyFileSync(skillSrc, skillDest);
|
|
283
|
+
console.log(` Created: ${SKILL_REL_PATH}`);
|
|
284
|
+
skillCopied++;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
200
288
|
// Create labels via gh
|
|
201
289
|
let labelsCreated = 0;
|
|
202
290
|
let labelsSkipReason = '';
|
|
@@ -259,12 +347,28 @@ function runInstall(options) {
|
|
|
259
347
|
}
|
|
260
348
|
|
|
261
349
|
// Record what got installed so `status` can report a version and detect
|
|
262
|
-
// drift. Only claim a version when the on-disk files actually match it
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
|
|
350
|
+
// drift. Only claim a version when the on-disk files actually match it.
|
|
351
|
+
//
|
|
352
|
+
// Two ways this can go wrong, both of which must block the claim:
|
|
353
|
+
// 1. A skip-mode install left older content on disk for something that
|
|
354
|
+
// WAS requested this run (tracked by the *Skipped counters below).
|
|
355
|
+
// 2. An --overwrite run touches only what was explicitly requested
|
|
356
|
+
// (workflows + scripts always; templates/skill only if their flags
|
|
357
|
+
// were passed) — templates or skill already on disk from an earlier
|
|
358
|
+
// install, but not requested this run, are left untouched and stale,
|
|
359
|
+
// even though --overwrite makes every *Skipped counter read 0. Naively
|
|
360
|
+
// trusting `overwrite` alone would then claim the whole install is
|
|
361
|
+
// current when part of it demonstrably wasn't touched.
|
|
362
|
+
const templatesPresentButNotTouched =
|
|
363
|
+
!withTemplates && TEMPLATES.some((t) => fs.existsSync(path.join(templatesDest, t)));
|
|
364
|
+
const skillPresentButNotTouched = !withSkill && fs.existsSync(skillPath(targetAbs));
|
|
365
|
+
const cleanInstall =
|
|
366
|
+
workflowsSkipped === 0 &&
|
|
367
|
+
templatesSkipped === 0 &&
|
|
368
|
+
scriptsSkipped === 0 &&
|
|
369
|
+
skillSkipped === 0 &&
|
|
370
|
+
!templatesPresentButNotTouched &&
|
|
371
|
+
!skillPresentButNotTouched;
|
|
268
372
|
if (!dryRun && cleanInstall) {
|
|
269
373
|
const pkgVersion = require(path.join(pkgRoot, 'package.json')).version;
|
|
270
374
|
writeManifest(targetAbs, pkgVersion);
|
|
@@ -273,18 +377,29 @@ function runInstall(options) {
|
|
|
273
377
|
// Summary
|
|
274
378
|
console.log('');
|
|
275
379
|
if (!dryRun && !cleanInstall) {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
380
|
+
if (templatesPresentButNotTouched || skillPresentButNotTouched) {
|
|
381
|
+
console.log(' Note: previously-installed templates and/or the Claude Code skill exist');
|
|
382
|
+
console.log(' on disk but were not requested this run, so the recorded Delivery OS');
|
|
383
|
+
console.log(' version was not updated. Re-run with --overwrite plus --with-templates');
|
|
384
|
+
console.log(' and/or --with-skill to bring everything (and the recorded version) in sync.');
|
|
385
|
+
} else {
|
|
386
|
+
console.log(' Note: some files already existed and were skipped, so the recorded');
|
|
387
|
+
console.log(' Delivery OS version was not updated. Re-run with --overwrite to sync');
|
|
388
|
+
console.log(' all files (and the recorded version) to the latest release.');
|
|
389
|
+
}
|
|
279
390
|
console.log('');
|
|
280
391
|
}
|
|
281
|
-
if (workflowsCopied > 0 || templatesCopied > 0 || labelsCreated > 0) {
|
|
392
|
+
if (workflowsCopied > 0 || templatesCopied > 0 || scriptsCopied > 0 || skillCopied > 0 || labelsCreated > 0) {
|
|
282
393
|
if (dryRun) {
|
|
283
394
|
if (workflowsCopied > 0) console.log(`Would install ${workflowsCopied} workflow(s).`);
|
|
284
395
|
if (templatesCopied > 0) console.log(`Would copy ${templatesCopied} issue template(s).`);
|
|
396
|
+
if (scriptsCopied > 0) console.log(`Would install ${scriptsCopied} supporting script(s).`);
|
|
397
|
+
if (skillCopied > 0) console.log('Would add the Claude Code delivery-ops skill.');
|
|
285
398
|
} else {
|
|
286
399
|
if (workflowsCopied > 0) console.log(`Installed ${workflowsCopied} workflow(s).`);
|
|
287
400
|
if (templatesCopied > 0) console.log(`Copied ${templatesCopied} issue template(s).`);
|
|
401
|
+
if (scriptsCopied > 0) console.log(`Installed ${scriptsCopied} supporting script(s).`);
|
|
402
|
+
if (skillCopied > 0) console.log('Added the Claude Code delivery-ops skill.');
|
|
288
403
|
if (labelsCreated > 0) console.log(`Created ${labelsCreated} label(s).`);
|
|
289
404
|
}
|
|
290
405
|
console.log('');
|
|
@@ -296,8 +411,14 @@ function runInstall(options) {
|
|
|
296
411
|
console.log(' - QA_APPROVER: GitHub username of QA approver');
|
|
297
412
|
console.log(' - QA_ASSIGNEES: Comma-separated usernames for QA assignment');
|
|
298
413
|
console.log(' 3. Add secrets (optional, for Telegram): TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID');
|
|
414
|
+
let nextStep = 4;
|
|
299
415
|
if (!withTemplates) {
|
|
300
|
-
console.log(
|
|
416
|
+
console.log(` ${nextStep}. Copy templates: re-run with --with-templates`);
|
|
417
|
+
nextStep++;
|
|
418
|
+
}
|
|
419
|
+
if (!withSkill) {
|
|
420
|
+
console.log(` ${nextStep}. Add the Claude Code delivery-ops skill (optional, for Claude Code users): re-run with --with-skill`);
|
|
421
|
+
nextStep++;
|
|
301
422
|
}
|
|
302
423
|
console.log('');
|
|
303
424
|
console.log('See https://phaneroo.github.io/github-delivery-operating-system/ for full docs.');
|
|
@@ -338,8 +459,20 @@ async function runStatus(options) {
|
|
|
338
459
|
const installedTemplates = TEMPLATES.filter((t) =>
|
|
339
460
|
fs.existsSync(path.join(templatesDest, t))
|
|
340
461
|
);
|
|
462
|
+
const skillInstalled = fs.existsSync(skillPath(targetAbs));
|
|
463
|
+
|
|
464
|
+
// A workflow can be present while the script it require()s at runtime is
|
|
465
|
+
// not — e.g. an install from before this check existed, or a manual
|
|
466
|
+
// partial copy. That workflow will fail (MODULE_NOT_FOUND) the next time
|
|
467
|
+
// it actually runs, silently, since nothing here executes the workflow
|
|
468
|
+
// itself to notice.
|
|
469
|
+
const brokenWorkflows = installedWorkflows.filter((wf) => {
|
|
470
|
+
const requiredScript = REQUIRED_SCRIPT_BY_WORKFLOW[wf];
|
|
471
|
+
if (!requiredScript) return false;
|
|
472
|
+
return !fs.existsSync(path.join(targetAbs, '.github', 'scripts', `${requiredScript}.js`));
|
|
473
|
+
});
|
|
341
474
|
|
|
342
|
-
if (installedWorkflows.length > 0 || installedTemplates.length > 0) {
|
|
475
|
+
if (installedWorkflows.length > 0 || installedTemplates.length > 0 || skillInstalled) {
|
|
343
476
|
const manifest = readManifest(targetAbs);
|
|
344
477
|
if (manifest && manifest.version) {
|
|
345
478
|
const installedOn = manifest.installedAt ? ` (installed ${manifest.installedAt.slice(0, 10)})` : '';
|
|
@@ -370,6 +503,17 @@ async function runStatus(options) {
|
|
|
370
503
|
installedWorkflows.forEach((wf) => console.log(` ✓ ${wf}.yml`));
|
|
371
504
|
console.log('');
|
|
372
505
|
}
|
|
506
|
+
|
|
507
|
+
if (brokenWorkflows.length > 0) {
|
|
508
|
+
console.log('⚠️ Broken install detected:');
|
|
509
|
+
brokenWorkflows.forEach((wf) => {
|
|
510
|
+
console.log(` ${wf}.yml requires .github/scripts/${REQUIRED_SCRIPT_BY_WORKFLOW[wf]}.js, which is missing.`);
|
|
511
|
+
});
|
|
512
|
+
console.log(' That workflow will fail with MODULE_NOT_FOUND the next time it runs.');
|
|
513
|
+
console.log(' Fix: npx github-delivery-os@latest install --overwrite .');
|
|
514
|
+
console.log('');
|
|
515
|
+
}
|
|
516
|
+
|
|
373
517
|
if (installedTemplates.length > 0) {
|
|
374
518
|
console.log('Templates:');
|
|
375
519
|
installedTemplates.forEach((t) => console.log(` ✓ ${t}`));
|
|
@@ -383,21 +527,33 @@ async function runStatus(options) {
|
|
|
383
527
|
console.log('');
|
|
384
528
|
}
|
|
385
529
|
|
|
386
|
-
if (installedWorkflows.length
|
|
530
|
+
if (installedWorkflows.length > 0 || installedTemplates.length > 0 || skillInstalled) {
|
|
531
|
+
console.log('Claude Code skill:');
|
|
532
|
+
console.log(
|
|
533
|
+
skillInstalled
|
|
534
|
+
? ' ✓ delivery-ops'
|
|
535
|
+
: ' ○ delivery-ops (not installed — re-run install with --with-skill)'
|
|
536
|
+
);
|
|
537
|
+
console.log('');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (installedWorkflows.length === 0 && installedTemplates.length === 0 && !skillInstalled) {
|
|
387
541
|
console.log('Delivery OS is not installed in this repository.');
|
|
388
542
|
console.log('Run: npx github-delivery-os install --with-templates .');
|
|
389
543
|
} else {
|
|
390
|
-
|
|
391
|
-
|
|
544
|
+
console.log(
|
|
545
|
+
`Summary: ${installedWorkflows.length}/${WORKFLOWS.length} workflows, ${installedTemplates.length}/${TEMPLATES.length} templates, skill: ${skillInstalled ? 'yes' : 'no'}`
|
|
546
|
+
);
|
|
392
547
|
}
|
|
393
548
|
console.log('');
|
|
394
549
|
}
|
|
395
550
|
|
|
396
551
|
function runUninstall(options) {
|
|
397
|
-
const { targetDir = '.', withTemplates = false, dryRun = false } = options;
|
|
552
|
+
const { targetDir = '.', withTemplates = false, withSkill = false, dryRun = false } = options;
|
|
398
553
|
const targetAbs = path.resolve(process.cwd(), targetDir);
|
|
399
554
|
const workflowsDest = path.join(targetAbs, '.github', 'workflows');
|
|
400
555
|
const templatesDest = path.join(targetAbs, '.github', 'ISSUE_TEMPLATE');
|
|
556
|
+
const scriptsDest = path.join(targetAbs, '.github', 'scripts');
|
|
401
557
|
|
|
402
558
|
console.log('=== GitHub Delivery Operating System — Uninstall ===');
|
|
403
559
|
console.log(`Target: ${targetAbs}`);
|
|
@@ -406,6 +562,7 @@ function runUninstall(options) {
|
|
|
406
562
|
|
|
407
563
|
let workflowsRemoved = 0;
|
|
408
564
|
let templatesRemoved = 0;
|
|
565
|
+
let scriptsRemoved = 0;
|
|
409
566
|
|
|
410
567
|
for (const wf of WORKFLOWS) {
|
|
411
568
|
const dest = path.join(workflowsDest, `${wf}.yml`);
|
|
@@ -420,6 +577,21 @@ function runUninstall(options) {
|
|
|
420
577
|
}
|
|
421
578
|
}
|
|
422
579
|
|
|
580
|
+
// Scripts are a required dependency of the workflows above, not optional,
|
|
581
|
+
// so (like workflows) they're always removed, not gated behind a flag.
|
|
582
|
+
for (const name of SCRIPTS) {
|
|
583
|
+
const dest = path.join(scriptsDest, `${name}.js`);
|
|
584
|
+
if (fs.existsSync(dest)) {
|
|
585
|
+
if (dryRun) {
|
|
586
|
+
console.log(` [dry-run] Would remove: .github/scripts/${name}.js`);
|
|
587
|
+
} else {
|
|
588
|
+
fs.unlinkSync(dest);
|
|
589
|
+
console.log(` Removed: .github/scripts/${name}.js`);
|
|
590
|
+
}
|
|
591
|
+
scriptsRemoved++;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
423
595
|
if (withTemplates) {
|
|
424
596
|
for (const t of TEMPLATES) {
|
|
425
597
|
const dest = path.join(templatesDest, t);
|
|
@@ -435,11 +607,39 @@ function runUninstall(options) {
|
|
|
435
607
|
}
|
|
436
608
|
}
|
|
437
609
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
610
|
+
let skillRemoved = 0;
|
|
611
|
+
if (withSkill) {
|
|
612
|
+
const skillDest = skillPath(targetAbs);
|
|
613
|
+
if (fs.existsSync(skillDest)) {
|
|
614
|
+
if (dryRun) {
|
|
615
|
+
console.log(` [dry-run] Would remove: ${SKILL_REL_PATH}`);
|
|
616
|
+
} else {
|
|
617
|
+
fs.unlinkSync(skillDest);
|
|
618
|
+
console.log(` Removed: ${SKILL_REL_PATH}`);
|
|
619
|
+
}
|
|
620
|
+
skillRemoved++;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Remove the version manifest too — but only once nothing Delivery-OS-
|
|
625
|
+
// related actually remains. Templates and the skill are kept by default
|
|
626
|
+
// (only removed with their own flags), and if they're still on disk, the
|
|
627
|
+
// manifest's version is still meaningful for them — deleting it would make
|
|
628
|
+
// a later `status` report "unknown version" for files that are, in fact,
|
|
629
|
+
// still fully present and version-tracked.
|
|
630
|
+
const anyWorkflowsRemain = WORKFLOWS.some((wf) => fs.existsSync(path.join(workflowsDest, `${wf}.yml`)));
|
|
631
|
+
const anyTemplatesRemain = TEMPLATES.some((t) => fs.existsSync(path.join(templatesDest, t)));
|
|
632
|
+
// Scripts are removed unconditionally just above, so this is normally
|
|
633
|
+
// always false by the time we get here — included anyway for the same
|
|
634
|
+
// reason the other three are checked explicitly rather than assumed:
|
|
635
|
+
// defensive completeness against a future change (e.g. a failed unlink,
|
|
636
|
+
// or script removal ever becoming flag-gated like templates/skill).
|
|
637
|
+
const anyScriptsRemain = SCRIPTS.some((name) => fs.existsSync(path.join(scriptsDest, `${name}.js`)));
|
|
638
|
+
const skillRemains = fs.existsSync(skillPath(targetAbs));
|
|
639
|
+
const nothingLeft = !anyWorkflowsRemain && !anyTemplatesRemain && !anyScriptsRemain && !skillRemains;
|
|
640
|
+
|
|
441
641
|
const manifestDest = manifestPath(targetAbs);
|
|
442
|
-
if (fs.existsSync(manifestDest)) {
|
|
642
|
+
if (nothingLeft && fs.existsSync(manifestDest)) {
|
|
443
643
|
if (dryRun) {
|
|
444
644
|
console.log(' [dry-run] Would remove: delivery-os.json');
|
|
445
645
|
} else {
|
|
@@ -449,14 +649,22 @@ function runUninstall(options) {
|
|
|
449
649
|
}
|
|
450
650
|
|
|
451
651
|
console.log('');
|
|
452
|
-
if (workflowsRemoved > 0 || templatesRemoved > 0) {
|
|
652
|
+
if (workflowsRemoved > 0 || templatesRemoved > 0 || skillRemoved > 0) {
|
|
653
|
+
const templateNote = withTemplates ? `, ${templatesRemoved} template(s)` : '';
|
|
654
|
+
const skillNote = withSkill ? `, ${skillRemoved} skill file(s)` : '';
|
|
453
655
|
if (dryRun) {
|
|
454
|
-
console.log(`Would remove ${workflowsRemoved} workflow(s)${
|
|
656
|
+
console.log(`Would remove ${workflowsRemoved} workflow(s)${templateNote}${skillNote}.`);
|
|
455
657
|
} else {
|
|
456
|
-
console.log(`Removed ${workflowsRemoved} workflow(s)${
|
|
658
|
+
console.log(`Removed ${workflowsRemoved} workflow(s)${templateNote}${skillNote}.`);
|
|
457
659
|
if (!withTemplates) {
|
|
458
660
|
console.log('Templates were kept. Re-run with --with-templates to remove them.');
|
|
459
661
|
}
|
|
662
|
+
if (!withSkill) {
|
|
663
|
+
console.log('Claude Code skill (if installed) was kept. Re-run with --with-skill to remove it.');
|
|
664
|
+
}
|
|
665
|
+
if (!nothingLeft) {
|
|
666
|
+
console.log('delivery-os.json was kept — something Delivery-OS-related is still on disk.');
|
|
667
|
+
}
|
|
460
668
|
}
|
|
461
669
|
} else {
|
|
462
670
|
console.log('No Delivery OS files found to remove.');
|
|
@@ -470,5 +678,16 @@ module.exports = {
|
|
|
470
678
|
runStatus,
|
|
471
679
|
runUninstall,
|
|
472
680
|
// Exposed for tests only — not part of the CLI's public API.
|
|
473
|
-
__test__: {
|
|
681
|
+
__test__: {
|
|
682
|
+
manifestPath,
|
|
683
|
+
readManifest,
|
|
684
|
+
writeManifest,
|
|
685
|
+
fetchLatestVersion,
|
|
686
|
+
skillPath,
|
|
687
|
+
SKILL_REL_PATH,
|
|
688
|
+
WORKFLOWS,
|
|
689
|
+
TEMPLATES,
|
|
690
|
+
SCRIPTS,
|
|
691
|
+
REQUIRED_SCRIPT_BY_WORKFLOW,
|
|
692
|
+
},
|
|
474
693
|
};
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
test:
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
steps:
|
|
12
|
-
- uses: actions/checkout@v6
|
|
13
|
-
|
|
14
|
-
- name: Set up Node.js
|
|
15
|
-
uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: '20'
|
|
18
|
-
|
|
19
|
-
- name: Install dependencies
|
|
20
|
-
run: npm ci
|
|
21
|
-
|
|
22
|
-
- name: Run tests
|
|
23
|
-
run: npm test
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Deploy docs/ to GitHub Pages
|
|
2
|
-
# Requires: Settings → Pages → Source: GitHub Actions
|
|
3
|
-
|
|
4
|
-
name: Deploy to GitHub Pages
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
branches: [main]
|
|
9
|
-
workflow_dispatch:
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
pages: write
|
|
14
|
-
id-token: write
|
|
15
|
-
|
|
16
|
-
concurrency:
|
|
17
|
-
group: pages
|
|
18
|
-
cancel-in-progress: false
|
|
19
|
-
|
|
20
|
-
env:
|
|
21
|
-
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
22
|
-
|
|
23
|
-
jobs:
|
|
24
|
-
deploy:
|
|
25
|
-
runs-on: ubuntu-latest
|
|
26
|
-
environment:
|
|
27
|
-
name: github-pages
|
|
28
|
-
url: ${{ steps.deployment.outputs.page_url }}
|
|
29
|
-
steps:
|
|
30
|
-
- name: Opt into Node.js 24
|
|
31
|
-
run: echo "FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true" >> $GITHUB_ENV
|
|
32
|
-
|
|
33
|
-
- name: Checkout
|
|
34
|
-
uses: actions/checkout@v6
|
|
35
|
-
|
|
36
|
-
- name: Setup Pages
|
|
37
|
-
uses: actions/configure-pages@v5
|
|
38
|
-
|
|
39
|
-
- name: Upload artifact
|
|
40
|
-
uses: actions/upload-pages-artifact@v4
|
|
41
|
-
with:
|
|
42
|
-
path: ./docs
|
|
43
|
-
|
|
44
|
-
- name: Deploy to GitHub Pages
|
|
45
|
-
id: deployment
|
|
46
|
-
uses: actions/deploy-pages@v4
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# Create GitHub release when a version tag is pushed
|
|
2
|
-
# Usage: git tag v1.0.2 && git push origin v1.0.2
|
|
3
|
-
|
|
4
|
-
name: Release
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
push:
|
|
8
|
-
tags:
|
|
9
|
-
- 'v*'
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: write
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
release:
|
|
16
|
-
runs-on: ubuntu-latest
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v6
|
|
19
|
-
with:
|
|
20
|
-
fetch-depth: 0
|
|
21
|
-
|
|
22
|
-
- name: Set up Node.js
|
|
23
|
-
uses: actions/setup-node@v4
|
|
24
|
-
with:
|
|
25
|
-
node-version: '20'
|
|
26
|
-
|
|
27
|
-
- name: Install dependencies
|
|
28
|
-
run: npm ci
|
|
29
|
-
|
|
30
|
-
- name: Run tests
|
|
31
|
-
run: npm test
|
|
32
|
-
|
|
33
|
-
- name: Create Release
|
|
34
|
-
uses: softprops/action-gh-release@v2
|
|
35
|
-
with:
|
|
36
|
-
generate_release_notes: true
|
|
37
|
-
env:
|
|
38
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|