@1aboveio/cicd-skills 0.1.14 → 0.1.15
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/package.json +2 -2
- package/skills/cloud-build/SKILL.md +1 -1
- package/skills/cloud-debug/SKILL.md +1 -1
- package/skills/cloud-deploy/SKILL.md +1 -1
- package/skills/docker/SKILL.md +1 -1
- package/skills/github-actions/SKILL.md +1 -1
- package/skills/github-flow/SKILL.md +10 -1
- package/skills/github-flow/scripts/persist-conventions-core.mjs +231 -21
- package/skills/github-flow/scripts/persist-conventions.mjs +18 -1
- package/skills/google-cloud/SKILL.md +1 -1
- package/skills/mergify/SKILL.md +1 -1
- package/skills/podman/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1aboveio/cicd-skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "GitHub Actions and flow, Mergify, Docker and Podman, Cloud Build, Cloud Deploy and Google Cloud.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"id": "cicd",
|
|
28
28
|
"core": "@1aboveio/core-skills",
|
|
29
29
|
"coreSkills": [],
|
|
30
|
-
"digest": "
|
|
30
|
+
"digest": "85efd2c1199e671d09ef0aea9d10bd11bea10de2271979e712b26130da33b2e8"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloud-build
|
|
3
3
|
description: "Build or debug Cloud Build pipelines for Cloud Run: cloudbuild.yaml, triggers, Artifact Registry, builds, migrations, deploys, secrets, VPC, notifications. Use cloud-deploy for staged promotion. NOT for GitHub Actions PR checks (github-actions)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Cloud Build Direct Deploy to Cloud Run
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cloud-debug
|
|
3
3
|
description: "Debug Cloud Build or Deploy failures: builds, deployments, health checks, and pipeline errors. Inputs: service, build ID, region. NOT for GitHub Actions runs (github-actions) or runtime errors in a running service (app-debug)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Cloud Build & Cloud Deploy Debug Skill
|
|
@@ -5,7 +5,7 @@ description: >
|
|
|
5
5
|
CI/CD pipelines with staged promotion, verification gates, or GitFlow branching.
|
|
6
6
|
Also use when debugging Cloud Deploy pipeline failures. NOT for simple single-container
|
|
7
7
|
deploys — use cloud-build instead.
|
|
8
|
-
release-version: 0.
|
|
8
|
+
release-version: 0.47.0
|
|
9
9
|
---
|
|
10
10
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
11
11
|
# Cloud Deploy + Skaffold + Kustomize for Cloud Run
|
package/skills/docker/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docker
|
|
3
3
|
description: Docker/Podman container architecture and build patterns. Read when designing or reviewing containerization, Dockerfile structure, multi-stage builds, PyPI mirror support, or docker-compose setup for Python/uv projects, and when a Dockerfile's dependency install misses the layer cache on every build (build args such as a commit SHA or version declared too early) or an image build is slow in CI.
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Docker Build Patterns
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-actions
|
|
3
3
|
description: "Author, review, audit, profile, optimize, or debug GitHub Actions workflows (.github/workflows/*.yml): run-history audits, job and step timing, resource bottlenecks, before/after comparisons, conditional CI by changed path, the one required gate check, PR and merge-queue lanes, sharding, workers, caching, self-hosted runners, timeouts, concurrency, permissions and action pinning, post-deploy verification, npm publishing. Use for 'set up CI', 'CI is slow', 'profile CI', 'optimize our pipeline', 'audit CI for redundant or wasted jobs', 'why is this check stuck expected or never started', 'harden this workflow', or 'browser not found on the runner'. NOT for pushing, opening, or merging a PR, or writing CI conventions into CONTRIBUTING.md (github-flow), queue rules or a check unmet inside Mergify (mergify), Playwright config and specs (e2e-test), cloudbuild.yaml or Cloud Run deploys (cloud-build), or which gates a repo must prove (ensure-coverage)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# GitHub Actions
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: github-flow
|
|
3
3
|
description: "Deliver a change after the commit: push, open a PR, wait for checks, and merge to main through the repo's flow (direct push, pull request, or merge queue). Use whenever you commit or finish a change, when CONTRIBUTING.md has a Delivery section, and for asks like 'commit this', 'push it', 'open a PR', 'ship it', 'land it', 'get it merged'. Also: declare a repo's delivery flow, or write the CI conventions and Delivery section into CONTRIBUTING.md on request. NOT for writing workflow YAML (github-actions), Mergify config or a stuck queued PR (mergify), cutting a release (resolve-release), or PRs that resolve-issues or rush-issues deliver."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# GitHub Flow
|
|
@@ -114,6 +114,15 @@ node "$SKILL_DIR/scripts/persist-conventions.mjs" --flow <flow> ... --write
|
|
|
114
114
|
- Adds a pointer to `AGENTS.md` unless it names `CONTRIBUTING.md`; creates missing files.
|
|
115
115
|
- Creates a missing `CLAUDE.md` as a symlink to `AGENTS.md`. Adds the pointer to a regular
|
|
116
116
|
`CLAUDE.md` that names neither file. Leaves a symlinked `CLAUDE.md` alone.
|
|
117
|
+
- Also adds one `## Delivery task entry` rule to `AGENTS.md`, between
|
|
118
|
+
`conventions:delivery-task-entry` markers, whichever section it writes: after a commit, or when
|
|
119
|
+
asked to push, open a PR, or merge, read and apply this skill. The rule is scoped to that task
|
|
120
|
+
and grants nothing: the user's request and the repo's declared policy still decide how far to go,
|
|
121
|
+
and `resolve-issues`/`rush-issues` deliver their own PRs. It is checked separately from the guide
|
|
122
|
+
pointer, so an `AGENTS.md` or regular `CLAUDE.md` that already links `CONTRIBUTING.md` still gets
|
|
123
|
+
it. An entrypoint that already loads the skill, a `CLAUDE.md` that points at `AGENTS.md`, or an
|
|
124
|
+
unmanaged section with that heading is preserved and reported rather than duplicated; a re-run
|
|
125
|
+
changes nothing.
|
|
117
126
|
- Report each file's result line. Committing the files is a change of its own: deliver it with this skill.
|
|
118
127
|
|
|
119
128
|
## Related skills
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
// A section sits between `conventions:<id>` markers, so a re-run replaces only
|
|
14
14
|
// that block and keeps everything else, including other skills' sections.
|
|
15
15
|
// Nothing is written without --write: the user triggers the write, not an agent.
|
|
16
|
+
//
|
|
17
|
+
// The guide pointer and a skill's task-entry loader are independent. A link to
|
|
18
|
+
// CONTRIBUTING.md says where the policy lives; it does not load the skill that applies
|
|
19
|
+
// it. So a repo whose AGENTS.md (or regular CLAUDE.md) already names the guide still
|
|
20
|
+
// gets the loader, and a repo that already loads the skill keeps its own rule.
|
|
21
|
+
// engineering's init.mjs reuses `withTaskEntries` for its own two loaders.
|
|
16
22
|
|
|
17
23
|
import fs from 'node:fs'
|
|
18
24
|
import path from 'node:path'
|
|
@@ -20,6 +26,8 @@ import path from 'node:path'
|
|
|
20
26
|
export const CONTRIBUTING_FILE = 'CONTRIBUTING.md'
|
|
21
27
|
const MENTIONS_CONTRIBUTING = /(?:^|[^\w-])CONTRIBUTING\.md/
|
|
22
28
|
|
|
29
|
+
const TASK_ENTRY = 'task-entry'
|
|
30
|
+
|
|
23
31
|
export const AGENTS_SECTION = [
|
|
24
32
|
'## Contributing',
|
|
25
33
|
'',
|
|
@@ -69,26 +77,218 @@ export function renderClaude(current) {
|
|
|
69
77
|
return `${current.replace(/\s*$/, '')}\n\n${AGENTS_SECTION}`
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
export function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
export function taskEntryMarkers(id) {
|
|
81
|
+
return [`<!-- conventions:${id}-${TASK_ENTRY}:start -->`, `<!-- conventions:${id}-${TASK_ENTRY}:end -->`]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Pure: the managed loader block. A short rule in an entrypoint; the conventions it
|
|
85
|
+
// points at stay in CONTRIBUTING.md.
|
|
86
|
+
export function taskEntryBlock({ id, title, body }) {
|
|
87
|
+
const [start, end] = taskEntryMarkers(id)
|
|
88
|
+
const recovery = body.includes('After context loss') ? '' : '\nAfter context loss, reload this skill\'s rules before continuing.'
|
|
89
|
+
return `${start}\n## ${title}\n\n${body.trim()}${recovery}\n${end}\n`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
93
|
+
function withoutExamples(text) {
|
|
94
|
+
let fence = null
|
|
95
|
+
let comment = false
|
|
96
|
+
const active = text.split('\n').map(line => {
|
|
97
|
+
if (fence) {
|
|
98
|
+
const match = /^ {0,3}(`{3,}|~{3,})/.exec(line)
|
|
99
|
+
if (match && match[1][0] === fence[0] && match[1].length >= fence.length
|
|
100
|
+
&& line.slice(match[0].length).trim() === '') fence = null
|
|
101
|
+
return ''
|
|
102
|
+
}
|
|
103
|
+
let masked = ''
|
|
104
|
+
let cursor = 0
|
|
105
|
+
while (cursor < line.length) {
|
|
106
|
+
const marker = line.indexOf(comment ? '-->' : '<!--', cursor)
|
|
107
|
+
const end = marker < 0 ? line.length : marker
|
|
108
|
+
masked += comment ? ' '.repeat(end - cursor) : line.slice(cursor, end)
|
|
109
|
+
if (marker < 0) break
|
|
110
|
+
const length = comment ? 3 : 4
|
|
111
|
+
masked += ' '.repeat(length)
|
|
112
|
+
cursor = marker + length
|
|
113
|
+
comment = !comment
|
|
114
|
+
}
|
|
115
|
+
const match = /^ {0,3}(`{3,}|~{3,})/.exec(masked)
|
|
116
|
+
if (match) { fence = match[1]; return '' }
|
|
117
|
+
return /^(?: {4}|\t|\s*>)/.test(line) ? '' : line
|
|
118
|
+
}).join('\n')
|
|
119
|
+
if (fence) throw new Error('unclosed Markdown code fence; repair guidance before setup')
|
|
120
|
+
if (comment) {
|
|
121
|
+
throw new Error('unclosed HTML comment; repair guidance before setup')
|
|
122
|
+
}
|
|
123
|
+
return active
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Keep code-form skill names, but not inline examples containing whole instructions.
|
|
127
|
+
const visible = (text) => withoutExamples(text).replace(/<!--[\s\S]*?-->/g, '')
|
|
128
|
+
.replace(/(`+)([^`]*?)\1/g, (span, _ticks, content) => /\s/.test(content) ? '' : span)
|
|
129
|
+
|
|
130
|
+
function hasHeading(text, title) {
|
|
131
|
+
return new RegExp(`^#{1,6}\\s+${escape(title)}\\s*$`, 'm').test(visible(text))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A hand-written equivalent ("read and apply the `test-quality` skill") already loads
|
|
135
|
+
// the skill, so the managed block would repeat it. A guide link never matches.
|
|
136
|
+
function loadsSkill(text, skill, contexts = []) {
|
|
137
|
+
const name = escape(skill)
|
|
138
|
+
const instruction = new RegExp(`\\b(?:read|load|apply|use|follow)\\s+(?:and\\s+(?:apply|read)\\s+)?(?:(?:the|installed)\\s+){0,2}(?:\`${name}\`|${name}\\s+skills?\\b)`, 'i')
|
|
139
|
+
return visible(text).split(/\n\s*\n/).some(paragraph =>
|
|
140
|
+
instruction.test(paragraph)
|
|
141
|
+
&& (!contexts.length || contexts.every(word => new RegExp(`\\b${escape(word)}\\b`, 'i').test(paragraph))))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Pure: `{ text, conflicts }` — each loader's block added once, where it is missing.
|
|
145
|
+
// An equivalent existing rule is preserved; malformed markers and an unmanaged section
|
|
146
|
+
// under the same heading are reported instead of written over.
|
|
147
|
+
export function withTaskEntries(current, loaders = []) {
|
|
148
|
+
let text = current
|
|
149
|
+
const conflicts = []
|
|
150
|
+
for (const loader of loaders) {
|
|
151
|
+
const body = visible(text)
|
|
152
|
+
const denied = new RegExp(`\\b(?:do not|don't|never|must not)\\s+(?:(?:automatically|ever)\\s+)?(?:read|load|apply|use|follow)\\b[^.\\n]{0,120}\\b${escape(loader.skill)}\\b`, 'i')
|
|
153
|
+
if (denied.test(body)) {
|
|
154
|
+
conflicts.push(`an existing rule forbids loading "${loader.skill}"; reconcile it before setup`)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
const restricted = new RegExp(`\\b${escape(loader.skill)}\\b[^.\\n]{0,100}\\bonly\\s+(?:when|if)\\s+(?:explicitly|the user)`, 'i')
|
|
158
|
+
if (restricted.test(body)) {
|
|
159
|
+
conflicts.push(`an existing explicit-only rule restricts "${loader.skill}"; reconcile it before setup`)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
if (loader.contexts?.length && visible(text).split(/\n\s*\n/).some(paragraph =>
|
|
163
|
+
loadsSkill(paragraph, loader.skill) && /\b(?:for|during)\b[^.\n]{0,80}\bonly\s*[,;:]/i.test(paragraph))) {
|
|
164
|
+
conflicts.push(`an existing task-only rule restricts "${loader.skill}"; reconcile it before setup`)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
const [start, end] = taskEntryMarkers(loader.id)
|
|
168
|
+
const active = withoutExamples(text)
|
|
169
|
+
const starts = active.split(start).length - 1
|
|
170
|
+
const ends = active.split(end).length - 1
|
|
171
|
+
if (starts || ends) {
|
|
172
|
+
// Already written: a re-run leaves the block as it is rather than refreshing it.
|
|
173
|
+
if (starts !== 1 || ends !== 1 || active.indexOf(start) > active.indexOf(end)) {
|
|
174
|
+
conflicts.push(`malformed "${loader.title}" markers; repair them by hand`)
|
|
175
|
+
} else if (!loadsSkill(active.slice(active.indexOf(start), active.indexOf(end)), loader.skill, loader.contexts)) {
|
|
176
|
+
conflicts.push(`the preserved "${loader.title}" block has no applicable skill loader; reconcile it before setup`)
|
|
177
|
+
}
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
if (loadsSkill(text, loader.skill, loader.contexts)) continue
|
|
181
|
+
if (hasHeading(text, loader.title)) {
|
|
182
|
+
conflicts.push(`an existing "${loader.title}" section needs manual reconciliation; no competing block added`)
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
text = `${text.replace(/\s*$/, '')}\n\n${taskEntryBlock(loader)}`
|
|
186
|
+
}
|
|
187
|
+
return { text, conflicts }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function indexEntries(section) {
|
|
191
|
+
const loaders = section.loader ? [section.loader] : []
|
|
192
|
+
return [
|
|
193
|
+
{ name: 'AGENTS.md', render: renderAgents, loaders },
|
|
194
|
+
// A regular CLAUDE.md that already points at AGENTS.md inherits that file's loaders.
|
|
195
|
+
{ name: 'CLAUDE.md', render: renderClaude, loaders, reachesAgents: true },
|
|
78
196
|
]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function reachesSelectedAgents(text, repoRoot, file) {
|
|
200
|
+
const body = visible(text)
|
|
201
|
+
const expected = path.join(repoRoot, 'AGENTS.md')
|
|
202
|
+
const canonical = name => fs.existsSync(name) ? fs.realpathSync(name) : path.resolve(name)
|
|
203
|
+
const affirmative = body.split('\n').filter(line => !/\b(?:do not|don't|never|must not)\b/i.test(line)).join('\n')
|
|
204
|
+
const references = [...affirmative.matchAll(/\[[^\]]*\]\((?:<([^>]+)>|([^\s)]+))\)/g)]
|
|
205
|
+
.map(match => match[1] ?? match[2])
|
|
206
|
+
for (const match of affirmative.matchAll(/\b(?:read|load|follow|see|consult)\s+`?((?:[^\s`]+\/)?AGENTS\.md)`?/gi)) {
|
|
207
|
+
references.push(match[1])
|
|
208
|
+
}
|
|
209
|
+
return references.some(ref => {
|
|
210
|
+
if (/^[a-z][a-z\d+.-]*:|^\/\//i.test(ref)) return false
|
|
211
|
+
try {
|
|
212
|
+
const target = path.resolve(path.dirname(file), decodeURIComponent(ref.split(/[?#]/)[0]))
|
|
213
|
+
return canonical(target) === canonical(expected)
|
|
214
|
+
} catch { return false }
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function apply(repoRoot, entries, write) {
|
|
219
|
+
repoRoot = fs.realpathSync(repoRoot)
|
|
220
|
+
const readText = file => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file))
|
|
221
|
+
const plannedContent = new Map()
|
|
222
|
+
const plan = entries.map((entry) => {
|
|
223
|
+
const file = path.join(repoRoot, entry.name)
|
|
224
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
225
|
+
let conflicts = []
|
|
226
|
+
try {
|
|
227
|
+
const target = fs.realpathSync(file)
|
|
228
|
+
const relative = path.relative(repoRoot, target)
|
|
229
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
230
|
+
conflicts = ['linked entrypoint is outside the selected project; verify its task loader separately']
|
|
231
|
+
} else {
|
|
232
|
+
const current = plannedContent.get(target) ?? readText(file)
|
|
233
|
+
const checked = withTaskEntries(current, entry.loaders)
|
|
234
|
+
conflicts = checked.conflicts
|
|
235
|
+
if (checked.text !== current && !(entry.reachesAgents && reachesSelectedAgents(current, repoRoot, target))) {
|
|
236
|
+
conflicts.push('linked entrypoint lacks the task loader; explicit reconciliation is required')
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch { conflicts = ['linked entrypoint could not be verified; setup is partial'] }
|
|
240
|
+
return { file: entry.name, action: 'unchanged', conflicts }
|
|
241
|
+
}
|
|
242
|
+
const current = fs.existsSync(file) ? readText(file) : null
|
|
243
|
+
const base = entry.render(current)
|
|
244
|
+
let next = base
|
|
245
|
+
let conflicts = []
|
|
246
|
+
if (base !== LINK_TO_AGENTS) {
|
|
247
|
+
const applied = withTaskEntries(base, entry.loaders)
|
|
248
|
+
const inherits = entry.reachesAgents && reachesSelectedAgents(current ?? '', repoRoot, file)
|
|
249
|
+
next = inherits ? base : applied.text
|
|
250
|
+
conflicts = applied.conflicts
|
|
251
|
+
}
|
|
85
252
|
if (next === LINK_TO_AGENTS) {
|
|
86
|
-
|
|
87
|
-
|
|
253
|
+
return { file: entry.name, action: 'link', conflicts, next }
|
|
254
|
+
}
|
|
255
|
+
const action = current == null ? 'create' : next === current ? 'unchanged' : 'update'
|
|
256
|
+
if (current !== null && action !== 'unchanged' && fs.statSync(file).nlink > 1) {
|
|
257
|
+
conflicts.push('hard-linked guidance cannot be changed without exclusive ownership')
|
|
88
258
|
}
|
|
89
|
-
|
|
90
|
-
return { file: name, action }
|
|
259
|
+
plannedContent.set(file, next)
|
|
260
|
+
return { file: entry.name, action, conflicts, next }
|
|
91
261
|
})
|
|
262
|
+
const blocked = plan.some(entry => entry.conflicts.length)
|
|
263
|
+
if (write && !blocked) {
|
|
264
|
+
for (const entry of plan.filter(entry => entry.action !== 'unchanged')) {
|
|
265
|
+
const file = path.join(repoRoot, entry.file)
|
|
266
|
+
fs.accessSync(entry.action === 'create' || entry.action === 'link' ? repoRoot : file, fs.constants.W_OK)
|
|
267
|
+
}
|
|
268
|
+
for (const entry of plan) {
|
|
269
|
+
const file = path.join(repoRoot, entry.file)
|
|
270
|
+
if (entry.next === LINK_TO_AGENTS) fs.symlinkSync('AGENTS.md', file)
|
|
271
|
+
else if (entry.action !== 'unchanged') fs.writeFileSync(file, entry.next)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return plan.map(({ next: _next, ...entry }) => ({
|
|
275
|
+
...entry, action: blocked && entry.action !== 'unchanged' ? 'blocked' : entry.action,
|
|
276
|
+
}))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function persist(repoRoot, section, { write = false } = {}) {
|
|
280
|
+
const block = sectionBlock(section)
|
|
281
|
+
const entries = [
|
|
282
|
+
{ name: CONTRIBUTING_FILE, render: (text) => renderConventions(text, section.id, block), loaders: [] },
|
|
283
|
+
...indexEntries(section),
|
|
284
|
+
]
|
|
285
|
+
return apply(repoRoot, entries, write)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The index half alone: for a section that is already present, only the guide pointer
|
|
289
|
+
// and the task-entry loader are due.
|
|
290
|
+
export function persistIndex(repoRoot, section, { write = false } = {}) {
|
|
291
|
+
return apply(repoRoot, indexEntries(section), write)
|
|
92
292
|
}
|
|
93
293
|
|
|
94
294
|
// `section` = { id, owner, title, body }. Exit 0 on success, 2 on a usage error.
|
|
@@ -107,12 +307,22 @@ export function runCli(argv, section, { cwd = process.cwd(), log = console.log }
|
|
|
107
307
|
log(`persist-conventions: not a directory: ${repoRoot}`)
|
|
108
308
|
return 2
|
|
109
309
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
310
|
+
let results
|
|
311
|
+
try { results = persist(repoRoot, section, { write }) }
|
|
312
|
+
catch (error) { log(`persist-conventions: setup refused: ${error.message}`); return 2 }
|
|
313
|
+
const done = { create: 'created', link: 'linked to AGENTS.md', update: 'updated', unchanged: 'unchanged', blocked: 'blocked' }
|
|
314
|
+
const conflicts = []
|
|
315
|
+
for (const r of results) {
|
|
316
|
+
for (const conflict of r.conflicts ?? []) conflicts.push(`${r.file}: ${conflict}`)
|
|
317
|
+
log(` ${r.file}: ${write || r.action === 'unchanged' ? done[r.action] : `would ${r.action}`}`)
|
|
318
|
+
}
|
|
319
|
+
const changed = results.filter((r) => !['unchanged', 'blocked'].includes(r.action)).length
|
|
114
320
|
log(write
|
|
115
321
|
? `persist-conventions: ${section.title}: ${changed} file(s) written, ${results.length - changed} unchanged`
|
|
116
322
|
: `persist-conventions: ${section.title}: preview only, ${changed} file(s) would change; re-run with --write to write`)
|
|
117
|
-
|
|
323
|
+
if (conflicts.length) {
|
|
324
|
+
for (const conflict of conflicts) log(` conflict: ${conflict}`)
|
|
325
|
+
log(`persist-conventions: ${section.title}: ${conflicts.length} conflict(s); no files written; reconcile before setup`)
|
|
326
|
+
}
|
|
327
|
+
return conflicts.length ? 2 : 0
|
|
118
328
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// persist-conventions — write the `## CI conventions` and `## Delivery` sections
|
|
2
2
|
// of a repo's CONTRIBUTING.md (../SKILL.md, Persist to CONTRIBUTING.md). Run only
|
|
3
3
|
// when the user asks; previews unless --write. Without --flow, writes CI conventions only.
|
|
4
|
+
// Either run also adds the skill's task-entry loader once to AGENTS.md (and to a
|
|
5
|
+
// regular CLAUDE.md that does not reach it).
|
|
4
6
|
//
|
|
5
7
|
// node persist-conventions.mjs [--repo-root <dir>] [--write]
|
|
6
8
|
// [--flow direct|pull-request|merge-queue] [--done merged|committed] [--merge-method squash|merge|rebase]
|
|
@@ -10,11 +12,26 @@ import { fileURLToPath } from 'node:url'
|
|
|
10
12
|
import { renderDelivery, validateDelivery } from './flow-core.mjs'
|
|
11
13
|
import { runCli } from './persist-conventions-core.mjs'
|
|
12
14
|
|
|
15
|
+
// One loader for both sections: whichever run happens first adds it, the other finds it.
|
|
16
|
+
const LOADER = {
|
|
17
|
+
id: 'delivery',
|
|
18
|
+
skill: 'github-flow',
|
|
19
|
+
title: 'Delivery task entry',
|
|
20
|
+
body: [
|
|
21
|
+
'After a commit, or when asked to push, open a pull request, or merge until the',
|
|
22
|
+
"change is done, read and apply the installed `github-flow` skill. The user's request",
|
|
23
|
+
"and the repository's declared delivery policy decide how far to go; loading it",
|
|
24
|
+
'authorizes no push or merge, and `resolve-issues` and `rush-issues` deliver their own',
|
|
25
|
+
'pull requests.',
|
|
26
|
+
].join('\n'),
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
const CI = {
|
|
14
30
|
id: 'ci',
|
|
15
31
|
owner: 'github-flow',
|
|
16
32
|
title: 'CI conventions',
|
|
17
33
|
body: fs.readFileSync(fileURLToPath(new URL('../references/ci-conventions.md', import.meta.url)), 'utf8'),
|
|
34
|
+
loader: LOADER,
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
const DELIVERY_FLAGS = { '--flow': 'flow', '--done': 'done', '--merge-method': 'mergeMethod' }
|
|
@@ -42,7 +59,7 @@ if (Object.keys(delivery).length) {
|
|
|
42
59
|
}
|
|
43
60
|
let code = deliveryOnly ? 0 : runCli(rest, CI)
|
|
44
61
|
if (code === 0 && delivery.flow) {
|
|
45
|
-
code = runCli(rest, { id: 'delivery', owner: 'github-flow', title: 'Delivery', body: renderDelivery(delivery) })
|
|
62
|
+
code = runCli(rest, { id: 'delivery', owner: 'github-flow', title: 'Delivery', body: renderDelivery(delivery), loader: LOADER })
|
|
46
63
|
} else if (code === 0) {
|
|
47
64
|
console.log('persist-conventions: Delivery: skipped; pass --flow to write it')
|
|
48
65
|
}
|
|
@@ -5,7 +5,7 @@ description: >
|
|
|
5
5
|
project management. Use when configuring GCP resources, managing service
|
|
6
6
|
accounts, setting up IAP access, configuring VPC networks, managing secrets,
|
|
7
7
|
or debugging cloud infrastructure.
|
|
8
|
-
release-version: 0.
|
|
8
|
+
release-version: 0.47.0
|
|
9
9
|
---
|
|
10
10
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
11
11
|
# Google Cloud Platform (GCP) Skill
|
package/skills/mergify/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mergify
|
|
3
3
|
description: "Mergify merge queue and stacked PRs (`mergify stack`): config, rules, queue status, delivery. Use whenever a merge queue or stack is involved, including plain asks ('watch #123 till it merges', 'where is #412 in the queue', 'push this stack'): `gh pr view`/`gh pr checks` cannot see a dequeue; use `mergify queue show` and the bundled watcher. Also: a hook saying 'use mergify stack push', a missing Change-Id; authoring or debugging `.mergify.yml`, queue rules, merge protections, batch size, the ruleset making merges queue-only; enqueue policy (`@mergifyio queue` vs `auto_merge_conditions`); a push dequeuing a queued PR; bypass actors; freezing a branch. Failures: a PR stuck, queued but never merging, or merged unqueued; CI green on GitHub but unmet in Mergify; a batch going red or bisecting; a queue check missing rather than failing. Outranks the vendor mergify-* skills. NOT for pushing a PR in a repo without stack hooks (github-flow), workflow YAML (github-actions), or GCP deploy pipelines (cloud-deploy)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Mergify Merge Queue
|
package/skills/podman/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: podman
|
|
3
3
|
description: "Debug, design, or fix Podman: rootless/rootful containers, pods, podman-compose, the socket and systemd services, Docker-compatible API (DOCKER_HOST/CONTAINER_HOST), networking, volumes, SELinux labels, Docker migration, and CI/SSH provisioning of rootless sockets (user lingering, XDG_RUNTIME_DIR). Covers FailedToOpenSocket and systemctl --user DBUS/XDG errors."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.47.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Podman Operations
|