@sdd-method/maintain-cli 0.1.0-rc.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/ci-binding/index.js +18 -7
- package/dist/lib/init/ci-workflow.js +15 -6
- package/dist/lib/lock-file.js +5 -0
- package/dist/lib/sync/index.js +11 -2
- package/dist/maintain-version.json +1 -1
- package/dist/verbs/sync.js +13 -1
- package/dist/verify-version.json +1 -1
- package/package.json +2 -2
|
@@ -18,6 +18,16 @@ import { DAGGER_VERSION, MODULE_CALL, MODULE_DIR, moduleSha256, renderModuleFile
|
|
|
18
18
|
export const CI_BINDINGS = ["github", "gitlab", "jenkins"];
|
|
19
19
|
/** M-ADR 0219 §8.2 (ruled 2026-10-16): what every SDD has before it chooses. */
|
|
20
20
|
export const DEFAULT_CI_BINDING = "github";
|
|
21
|
+
/**
|
|
22
|
+
* Runner labels (sync --ci-runner): where the host runs the job. One label is a word of letters,
|
|
23
|
+
* digits, '.', '_' or '-', so a label can never inject YAML or Groovy into a wrapper.
|
|
24
|
+
*/
|
|
25
|
+
export const CI_RUNNER_DEFAULT = "default";
|
|
26
|
+
const RUNNER_LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
27
|
+
export function parseCiRunner(s) {
|
|
28
|
+
const labels = s.split(",").map((l) => l.trim());
|
|
29
|
+
return labels.length > 0 && labels.every((l) => RUNNER_LABEL.test(l)) ? labels : null;
|
|
30
|
+
}
|
|
21
31
|
export function isCiBinding(s) {
|
|
22
32
|
return CI_BINDINGS.includes(s);
|
|
23
33
|
}
|
|
@@ -42,13 +52,14 @@ function header(comment, profile) {
|
|
|
42
52
|
// GitLab: a Docker-in-Docker service, the shape GitLab documents for Dagger. The runner must allow
|
|
43
53
|
// privileged containers; that is the adopter's infrastructure (M-ADR 0219 §4.5). No `stage:`, so
|
|
44
54
|
// the job lands in the default `test` stage whatever stages the adopter declares around it.
|
|
45
|
-
function gitlabBlock(profile) {
|
|
55
|
+
function gitlabBlock(profile, runner) {
|
|
46
56
|
return [
|
|
47
57
|
HASH.begin,
|
|
48
58
|
...header("#", profile),
|
|
49
59
|
"",
|
|
50
60
|
"method-validation:",
|
|
51
61
|
" image: docker:27.3.1",
|
|
62
|
+
...(runner !== undefined && runner.length > 0 ? [` tags: [${runner.join(", ")}]`] : []),
|
|
52
63
|
" services:",
|
|
53
64
|
" - docker:27.3.1-dind",
|
|
54
65
|
" variables:",
|
|
@@ -70,13 +81,13 @@ function gitlabBlock(profile) {
|
|
|
70
81
|
// Jenkins: a declarative pipeline at its own path, so it never collides with an adopter's
|
|
71
82
|
// Jenkinsfile; the job's Script Path points at it. The agent must reach a Docker daemon. Dagger is
|
|
72
83
|
// installed outside the workspace, so the binary is not uploaded with --source.
|
|
73
|
-
function jenkinsBlock(profile) {
|
|
84
|
+
function jenkinsBlock(profile, runner) {
|
|
74
85
|
return [
|
|
75
86
|
SLASH.begin,
|
|
76
87
|
...header("//", profile),
|
|
77
88
|
"",
|
|
78
89
|
"pipeline {",
|
|
79
|
-
" agent any",
|
|
90
|
+
runner !== undefined && runner.length > 0 ? ` agent { label '${runner.join(" && ")}' }` : " agent any",
|
|
80
91
|
" stages {",
|
|
81
92
|
" stage('Method Validation') {",
|
|
82
93
|
" steps {",
|
|
@@ -100,13 +111,13 @@ export const HOST_WRAPPERS = {
|
|
|
100
111
|
relPath: ".gitlab-ci.yml",
|
|
101
112
|
markers: HASH,
|
|
102
113
|
block: gitlabBlock,
|
|
103
|
-
file: (p) => gitlabBlock(p) + "\n\n# Adopter-specific jobs go below, outside the method-managed markers.\n",
|
|
114
|
+
file: (p, r) => gitlabBlock(p, r) + "\n\n# Adopter-specific jobs go below, outside the method-managed markers.\n",
|
|
104
115
|
},
|
|
105
116
|
jenkins: {
|
|
106
117
|
relPath: ".jenkins/method-validation.Jenkinsfile",
|
|
107
118
|
markers: SLASH,
|
|
108
119
|
block: jenkinsBlock,
|
|
109
|
-
file: (p) => jenkinsBlock(p) + "\n",
|
|
120
|
+
file: (p, r) => jenkinsBlock(p, r) + "\n",
|
|
110
121
|
},
|
|
111
122
|
};
|
|
112
123
|
/** sha256 of a host file's method-managed block (begin marker through end marker), or null without one. */
|
|
@@ -128,7 +139,7 @@ export async function syncCiBinding(o) {
|
|
|
128
139
|
// 1. The host wrapper.
|
|
129
140
|
const wrapperAbs = join(o.targetRepo, host.relPath);
|
|
130
141
|
const existing = await tryRead(wrapperAbs);
|
|
131
|
-
const block = host.block(o.profile);
|
|
142
|
+
const block = host.block(o.profile, o.runner);
|
|
132
143
|
const drifted = existing !== null && detectBlockDrift(existing, block, host.markers);
|
|
133
144
|
const recorded = sameBinding ? o.prevLock?.ciWorkflowBlockSha256 : undefined;
|
|
134
145
|
const pristine = existing !== null && recorded !== undefined && blockSha256(existing, host.markers) === recorded;
|
|
@@ -159,7 +170,7 @@ export async function syncCiBinding(o) {
|
|
|
159
170
|
if (o.checkOnly || failures.length > 0) {
|
|
160
171
|
return { failures, log, wrapperSha: null, moduleSha: moduleSha256(onDisk) };
|
|
161
172
|
}
|
|
162
|
-
const written = await writeMarkedBlockFile(wrapperAbs, existing === null ? host.file(o.profile) : block, host.markers, o.allowOverwrite);
|
|
173
|
+
const written = await writeMarkedBlockFile(wrapperAbs, existing === null ? host.file(o.profile, o.runner) : block, host.markers, o.allowOverwrite);
|
|
163
174
|
if (written.changed)
|
|
164
175
|
log.push(`${existing === null ? "Wrote" : "Refreshed"} ${host.relPath}.`);
|
|
165
176
|
if (!matches) {
|
|
@@ -16,8 +16,8 @@ export const BLOCK_END = "# method-managed: end";
|
|
|
16
16
|
* Full file content (block + adopter-extension footer). Used by
|
|
17
17
|
* `sdd-cli init` to write a fresh workflow.
|
|
18
18
|
*/
|
|
19
|
-
export function renderMethodValidationWorkflow(profile) {
|
|
20
|
-
const methodBlock = buildMethodManagedBlock(profile);
|
|
19
|
+
export function renderMethodValidationWorkflow(profile, runner) {
|
|
20
|
+
const methodBlock = buildMethodManagedBlock(profile, runner);
|
|
21
21
|
return methodBlock + "\n\n# Adopter-specific jobs go in a separate workflow file\n# (e.g. .github/workflows/adopter-validation.yml) — see\n# docs/method/methods/ci-baseline-guide.md.\n";
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -26,10 +26,19 @@ export function renderMethodValidationWorkflow(profile) {
|
|
|
26
26
|
* `writeMarkedBlockFile` so adopter content outside the markers is
|
|
27
27
|
* preserved verbatim.
|
|
28
28
|
*/
|
|
29
|
-
export function renderMethodManagedWorkflowBlock(profile) {
|
|
30
|
-
return buildMethodManagedBlock(profile);
|
|
29
|
+
export function renderMethodManagedWorkflowBlock(profile, runner) {
|
|
30
|
+
return buildMethodManagedBlock(profile, runner);
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* `runs-on`: GitHub's hosted runner unless the SDD chose labels (sync --ci-runner), e.g. a
|
|
34
|
+
* self-hosted runner, where the Dagger engine and its cache persist between runs.
|
|
35
|
+
*/
|
|
36
|
+
function runsOn(runner) {
|
|
37
|
+
if (runner === undefined || runner.length === 0)
|
|
38
|
+
return "ubuntu-latest";
|
|
39
|
+
return runner.length === 1 ? runner[0] : `[${runner.join(", ")}]`;
|
|
40
|
+
}
|
|
41
|
+
function buildMethodManagedBlock(profile, runner) {
|
|
33
42
|
return [
|
|
34
43
|
BLOCK_BEGIN,
|
|
35
44
|
"# Method Validation per M-ADR 0129, run by the method's Dagger module (M-ADR 0219).",
|
|
@@ -63,7 +72,7 @@ function buildMethodManagedBlock(profile) {
|
|
|
63
72
|
// ONE job: GitHub bills each job rounded UP to a whole minute (adopter measurement, 2026-10-12).
|
|
64
73
|
" method-validation:",
|
|
65
74
|
" name: Method Validation",
|
|
66
|
-
|
|
75
|
+
` runs-on: ${runsOn(runner)}`,
|
|
67
76
|
" steps:",
|
|
68
77
|
" - uses: actions/checkout@v4",
|
|
69
78
|
" - name: Install Dagger",
|
package/dist/lib/lock-file.js
CHANGED
|
@@ -63,6 +63,7 @@ export function serialiseLockFile(lock) {
|
|
|
63
63
|
`method_repo: ${lock.methodRepo}`,
|
|
64
64
|
`lock_version: ${lock.lockVersion}`,
|
|
65
65
|
...(lock.ciBinding !== undefined ? [`ci_binding: ${lock.ciBinding}`] : []),
|
|
66
|
+
...(lock.ciRunner !== undefined ? [`ci_runner: "${lock.ciRunner}"`] : []),
|
|
66
67
|
...(lock.ciWorkflowBlockSha256 !== undefined ? [`ci_workflow_block_sha256: "${lock.ciWorkflowBlockSha256}"`] : []),
|
|
67
68
|
...(lock.ciModuleSha256 !== undefined ? [`ci_module_sha256: "${lock.ciModuleSha256}"`] : []),
|
|
68
69
|
"bundles:",
|
|
@@ -114,6 +115,7 @@ function parseVersioned(raw) {
|
|
|
114
115
|
let lockVersion;
|
|
115
116
|
let ciWorkflowBlockSha256;
|
|
116
117
|
let ciBinding;
|
|
118
|
+
let ciRunner;
|
|
117
119
|
let ciModuleSha256;
|
|
118
120
|
const bundles = [];
|
|
119
121
|
let inBundles = false;
|
|
@@ -180,6 +182,8 @@ function parseVersioned(raw) {
|
|
|
180
182
|
ciWorkflowBlockSha256 = stripQuotes(value);
|
|
181
183
|
else if (key === "ci_binding")
|
|
182
184
|
ciBinding = stripQuotes(value);
|
|
185
|
+
else if (key === "ci_runner")
|
|
186
|
+
ciRunner = stripQuotes(value);
|
|
183
187
|
else if (key === "ci_module_sha256")
|
|
184
188
|
ciModuleSha256 = stripQuotes(value);
|
|
185
189
|
}
|
|
@@ -306,6 +310,7 @@ function parseVersioned(raw) {
|
|
|
306
310
|
lockVersion: LOCK_VERSION,
|
|
307
311
|
bundles,
|
|
308
312
|
...(ciBinding !== undefined ? { ciBinding } : {}),
|
|
313
|
+
...(ciRunner !== undefined ? { ciRunner } : {}),
|
|
309
314
|
...(ciWorkflowBlockSha256 !== undefined ? { ciWorkflowBlockSha256 } : {}),
|
|
310
315
|
...(ciModuleSha256 !== undefined ? { ciModuleSha256 } : {}),
|
|
311
316
|
};
|
package/dist/lib/sync/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { syncMethodManagedCopilotHooks } from "./copilot-hooks-merge.js";
|
|
|
15
15
|
import { syncBriefingFiles } from "./briefing-merge.js";
|
|
16
16
|
import { DEFAULT_AGENT_HARNESS } from "../agent-harness.js";
|
|
17
17
|
import { BLOCK_BEGIN, BLOCK_END } from "../init/ci-workflow.js";
|
|
18
|
-
import { blockSha256, CI_BINDINGS, DEFAULT_CI_BINDING, isCiBinding, syncCiBinding, } from "../ci-binding/index.js";
|
|
18
|
+
import { blockSha256, CI_BINDINGS, DEFAULT_CI_BINDING, isCiBinding, parseCiRunner, syncCiBinding, } from "../ci-binding/index.js";
|
|
19
19
|
import { BRIEFING_TEMPLATE_ROOT, WRAPPER_BLOCK_BEGIN, WRAPPER_BLOCK_END, harnessWrapperOutputPath, } from "../briefing/index.js";
|
|
20
20
|
const SECTION_RULE = "═".repeat(59);
|
|
21
21
|
/**
|
|
@@ -97,6 +97,12 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
97
97
|
return { exitCode: 1 };
|
|
98
98
|
}
|
|
99
99
|
const ciBinding = opts.ciBinding ?? lockBinding ?? DEFAULT_CI_BINDING;
|
|
100
|
+
const lockRunner = prevLock?.ciRunner === undefined ? undefined : parseCiRunner(prevLock.ciRunner);
|
|
101
|
+
if (lockRunner === null) {
|
|
102
|
+
stdout(fail(`method-baseline.lock names ci_runner '${prevLock.ciRunner}', which is not a list of runner labels. Re-run with --ci-runner <label[,label]> or --ci-runner default.`));
|
|
103
|
+
return { exitCode: 1 };
|
|
104
|
+
}
|
|
105
|
+
const ciRunner = opts.ciRunner ?? lockRunner;
|
|
100
106
|
const classification = await classifyConflicts(conflicts, opts.targetRepo, prevEntry?.pathDigests);
|
|
101
107
|
if (conflicts.length > 0) {
|
|
102
108
|
reportClassification(stdout, conflicts.length, classification, prevEntry?.bundleVersion);
|
|
@@ -360,6 +366,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
360
366
|
targetRepo: opts.targetRepo,
|
|
361
367
|
profile: preflightProfile,
|
|
362
368
|
binding: ciBinding,
|
|
369
|
+
...(ciRunner !== undefined ? { runner: ciRunner } : {}),
|
|
363
370
|
prevLock,
|
|
364
371
|
allowDrift: false,
|
|
365
372
|
allowOverwrite: false,
|
|
@@ -564,6 +571,7 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
564
571
|
targetRepo: opts.targetRepo,
|
|
565
572
|
profile,
|
|
566
573
|
binding: ciBinding,
|
|
574
|
+
...(ciRunner !== undefined ? { runner: ciRunner } : {}),
|
|
567
575
|
prevLock,
|
|
568
576
|
allowDrift: opts.allowOverwrite || migrateFrom !== null,
|
|
569
577
|
allowOverwrite: opts.allowOverwrite,
|
|
@@ -596,10 +604,11 @@ export async function runSync(opts, stdout = (line) => process.stdout.write(line
|
|
|
596
604
|
// Record the wrapper block and the module exactly as they now stand on disk, so the next sync
|
|
597
605
|
// can tell them untouched from edited (M-ADR 0218 §4.7), and the binding when it is not the default.
|
|
598
606
|
const nextLock = upsertBundle(migrateFrom ? removeBundle(prevLock, migrateFrom.name) : prevLock, newEntry);
|
|
599
|
-
const { ciBinding: _previousBinding, ciWorkflowBlockSha256: _previousSha, ...lockBase } = nextLock;
|
|
607
|
+
const { ciBinding: _previousBinding, ciRunner: _previousRunner, ciWorkflowBlockSha256: _previousSha, ...lockBase } = nextLock;
|
|
600
608
|
await writeLockFile(opts.targetRepo, {
|
|
601
609
|
...lockBase,
|
|
602
610
|
...(ciBinding !== DEFAULT_CI_BINDING ? { ciBinding } : {}),
|
|
611
|
+
...(ciRunner !== undefined && ciRunner.length > 0 ? { ciRunner: ciRunner.join(",") } : {}),
|
|
603
612
|
...(ci.wrapperSha !== null ? { ciWorkflowBlockSha256: ci.wrapperSha } : {}),
|
|
604
613
|
ciModuleSha256: ci.moduleSha,
|
|
605
614
|
});
|
package/dist/verbs/sync.js
CHANGED
|
@@ -2,7 +2,7 @@ import { AGENT_HARNESSES, resolveAgentHarness, } from "../lib/agent-harness.js";
|
|
|
2
2
|
import { loadSyncConfig } from "../lib/sync/config.js";
|
|
3
3
|
import { materialiseBundle } from "../lib/sync/bundle-source.js";
|
|
4
4
|
import { runSync } from "../lib/sync/index.js";
|
|
5
|
-
import { CI_BINDINGS, isCiBinding } from "../lib/ci-binding/index.js";
|
|
5
|
+
import { CI_BINDINGS, CI_RUNNER_DEFAULT, isCiBinding, parseCiRunner } from "../lib/ci-binding/index.js";
|
|
6
6
|
import { resolvePathOption } from "./path-option.js";
|
|
7
7
|
export function registerSync(program) {
|
|
8
8
|
program
|
|
@@ -18,12 +18,22 @@ export function registerSync(program) {
|
|
|
18
18
|
.option("--no-delete", "preserve files removed upstream")
|
|
19
19
|
.option("--agent-harness <name>", `target agent harness for hooks + briefing. Default: claude-code. Supported: ${AGENT_HARNESSES.join(", ")}`)
|
|
20
20
|
.option("--ci-binding <host>", `CI host whose wrapper calls the Method Validation module (M-ADR 0219): ${CI_BINDINGS.join(", ")}. Default: the binding the lock records, else github; a new choice is recorded in the lock`)
|
|
21
|
+
.option("--ci-runner <labels>", `runner label(s) the CI wrapper targets, comma-separated (GitHub runs-on, GitLab tags, Jenkins agent label), e.g. a self-hosted runner. '${CI_RUNNER_DEFAULT}' returns to the host's default runner. Default: what the lock records; a new choice is recorded`)
|
|
21
22
|
.action(async (opts) => {
|
|
22
23
|
if (opts.ciBinding !== undefined && !isCiBinding(opts.ciBinding)) {
|
|
23
24
|
process.stderr.write(`[FAIL] --ci-binding must be one of ${CI_BINDINGS.join(", ")}; got '${opts.ciBinding}'\n`);
|
|
24
25
|
process.exit(1);
|
|
25
26
|
}
|
|
26
27
|
const ciBinding = opts.ciBinding;
|
|
28
|
+
let ciRunner;
|
|
29
|
+
if (opts.ciRunner !== undefined) {
|
|
30
|
+
const parsed = opts.ciRunner === CI_RUNNER_DEFAULT ? [] : parseCiRunner(opts.ciRunner);
|
|
31
|
+
if (parsed === null) {
|
|
32
|
+
process.stderr.write(`[FAIL] --ci-runner takes comma-separated labels of letters, digits, '.', '_' or '-', or '${CI_RUNNER_DEFAULT}'; got '${opts.ciRunner}'\n`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
ciRunner = parsed;
|
|
36
|
+
}
|
|
27
37
|
let agentHarness;
|
|
28
38
|
try {
|
|
29
39
|
agentHarness = resolveAgentHarness(opts.agentHarness);
|
|
@@ -50,6 +60,7 @@ export function registerSync(program) {
|
|
|
50
60
|
delete: opts.delete,
|
|
51
61
|
agentHarness,
|
|
52
62
|
...(ciBinding ? { ciBinding } : {}),
|
|
63
|
+
...(ciRunner ? { ciRunner } : {}),
|
|
53
64
|
...(opts.replaceBundle ? { replaceBundle: opts.replaceBundle } : {}),
|
|
54
65
|
...(opts.yes === true ? { yes: true } : {}),
|
|
55
66
|
});
|
|
@@ -78,6 +89,7 @@ export function registerSync(program) {
|
|
|
78
89
|
delete: opts.delete,
|
|
79
90
|
agentHarness,
|
|
80
91
|
...(ciBinding ? { ciBinding } : {}),
|
|
92
|
+
...(ciRunner ? { ciRunner } : {}),
|
|
81
93
|
});
|
|
82
94
|
if (exitCode !== 0) {
|
|
83
95
|
await resolved.cleanup();
|
package/dist/verify-version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdd-method/maintain-cli",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"sddCliVersion": "1.42.0",
|
|
5
|
-
"description": "Bring an SDD repository current and rebuild its derived model
|
|
5
|
+
"description": "Bring an SDD repository current and rebuild its derived model — sync, briefing, catalogue build and aggregate, pack build, context pull — plus every read-only verify verb. Ships only the code those verbs reach; it cannot author specification content. Authoring is in @sdd-method/sdd-cli.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|