@1aboveio/cicd-skills 0.1.13 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1aboveio/cicd-skills",
3
- "version": "0.1.13",
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": "448c38a77150a7a8de694cca11632434acb452acada143aeba540631b376c248"
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.45.2
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.45.2
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.45.2
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
@@ -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.45.2
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.45.2
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.45.2
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 persist(repoRoot, section, { write = false } = {}) {
73
- const block = sectionBlock(section)
74
- const plan = [
75
- [CONTRIBUTING_FILE, (text) => renderConventions(text, section.id, block)],
76
- ['AGENTS.md', renderAgents],
77
- ['CLAUDE.md', renderClaude],
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
- return plan.map(([name, render]) => {
80
- const file = path.join(repoRoot, name)
81
- if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) return { file: name, action: 'unchanged' }
82
- const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null
83
- const next = render(current)
84
- const action = current == null ? 'create' : next === current ? 'unchanged' : 'update'
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
- if (write) fs.symlinkSync('AGENTS.md', file)
87
- return { file: name, action: 'link' }
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
- if (write && action !== 'unchanged') fs.writeFileSync(file, next)
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
- const results = persist(repoRoot, section, { write })
111
- const done = { create: 'created', link: 'linked to AGENTS.md', update: 'updated', unchanged: 'unchanged' }
112
- for (const r of results) log(` ${r.file}: ${write || r.action === 'unchanged' ? done[r.action] : `would ${r.action}`}`)
113
- const changed = results.filter((r) => r.action !== 'unchanged').length
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
- return 0
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.45.2
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
@@ -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.45.2
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
@@ -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.45.2
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