release-skill 0.2.9 → 0.4.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +41 -0
- package/INSTALL.md +34 -110
- package/INSTALL.zh-CN.md +17 -83
- package/README.md +49 -41
- package/README.zh-CN.md +43 -34
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/claude/schemas/release-plan.schema.json +7 -0
- package/adapters/claude/schemas/release-run.schema.json +43 -0
- package/adapters/claude/skills/release-help/SKILL.md +10 -2
- package/adapters/claude/skills/release-prepare/SKILL.md +6 -15
- package/adapters/claude/skills/release-publish/SKILL.md +5 -6
- package/adapters/claude/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/claude/skills/release-verify/SKILL.md +4 -6
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/codex/schemas/release-plan.schema.json +7 -0
- package/adapters/codex/schemas/release-run.schema.json +43 -0
- package/adapters/codex/skills/release-help/SKILL.md +10 -2
- package/adapters/codex/skills/release-prepare/SKILL.md +6 -15
- package/adapters/codex/skills/release-publish/SKILL.md +5 -6
- package/adapters/codex/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/codex/skills/release-verify/SKILL.md +4 -6
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/kimi/schemas/release-plan.schema.json +7 -0
- package/adapters/kimi/schemas/release-run.schema.json +43 -0
- package/adapters/kimi/skills/release-help/SKILL.md +10 -2
- package/adapters/kimi/skills/release-prepare/SKILL.md +6 -15
- package/adapters/kimi/skills/release-publish/SKILL.md +5 -6
- package/adapters/kimi/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -6
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4131 -3180
- package/adapters/workbuddy/schemas/release-plan.schema.json +7 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +43 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +10 -2
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +6 -15
- package/adapters/workbuddy/skills/release-publish/SKILL.md +5 -6
- package/adapters/workbuddy/skills/release-reconcile/SKILL.md +2 -2
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -6
- package/bin/release-skill-cli.mjs +212 -43
- package/bin/release-skill.bundle.mjs +4131 -3180
- package/package.json +1 -1
- package/references/01-state-machine.md +1 -1
- package/references/06-adapter-contract.md +6 -5
- package/schemas/release-plan.schema.json +7 -0
- package/schemas/release-run.schema.json +43 -0
- package/skills/release-help/SKILL.md +10 -2
- package/skills/release-prepare/SKILL.md +6 -15
- package/skills/release-publish/SKILL.md +5 -6
- package/skills/release-reconcile/SKILL.md +2 -2
- package/skills/release-verify/SKILL.md +4 -6
- package/skills-src/release-help/SKILL.md +10 -2
- package/skills-src/release-prepare/SKILL.md +6 -15
- package/skills-src/release-publish/SKILL.md +5 -6
- package/skills-src/release-reconcile/SKILL.md +2 -2
- package/skills-src/release-verify/SKILL.md +4 -6
- package/src/adapters/plugin-marketplace.mjs +1 -1
- package/src/adapters/push-snapshot.mjs +3 -1
- package/src/commands/approve.mjs +8 -6
- package/src/commands/attest.mjs +195 -0
- package/src/commands/hooks.mjs +42 -0
- package/src/commands/prepare.mjs +13 -50
- package/src/commands/publish.mjs +0 -8
- package/src/commands/reconcile.mjs +3 -37
- package/src/commands/ship.mjs +334 -0
- package/src/commands/verify.mjs +195 -15
- package/src/core/git-transport.mjs +93 -0
- package/src/core/release-metadata.mjs +105 -0
- package/src/core/skill-resource-closure.mjs +7 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { ReleaseError, REMOTE_UNAVAILABLE, REMOTE_CONFLICT } from './errors.mjs';
|
|
5
|
+
|
|
6
|
+
const execFile = promisify(execFileCb);
|
|
7
|
+
|
|
8
|
+
function urls(repo, host) {
|
|
9
|
+
if (typeof repo !== 'string' || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
|
|
10
|
+
throw new ReleaseError(REMOTE_UNAVAILABLE, 'git transport preflight requires owner/name repositories');
|
|
11
|
+
}
|
|
12
|
+
const safeHost = host ?? 'github.com';
|
|
13
|
+
if (!/^[A-Za-z0-9.-]+$/.test(safeHost)) {
|
|
14
|
+
throw new ReleaseError(REMOTE_UNAVAILABLE, 'git transport preflight requires a safe GitHub hostname');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
https: `https://${safeHost}/${repo}.git`,
|
|
18
|
+
ssh: `git@${safeHost}:${repo}.git`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function identity(stdout) {
|
|
23
|
+
const lines = String(stdout).trim().split('\n').filter(Boolean);
|
|
24
|
+
const head = lines.find((line) => /^[a-f0-9]{40,64}\s+HEAD$/.test(line.trim()));
|
|
25
|
+
const symref = lines.find((line) => line.startsWith('ref: '));
|
|
26
|
+
return `${symref ?? ''}\n${head ?? ''}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function preflightGitTransports(plan, options = {}) {
|
|
30
|
+
const exec = options.exec ?? ((command, args, execOptions) => execFile(command, args, execOptions));
|
|
31
|
+
const repositories = new Map();
|
|
32
|
+
for (const action of plan.externalActions ?? []) {
|
|
33
|
+
const repo = action.parameters?.repo;
|
|
34
|
+
if (!repo) continue;
|
|
35
|
+
const host = action.parameters?.githubHost ?? 'github.com';
|
|
36
|
+
repositories.set(`${host}/${repo}`, { repo, host });
|
|
37
|
+
}
|
|
38
|
+
const observations = [];
|
|
39
|
+
for (const { repo, host } of repositories.values()) {
|
|
40
|
+
const candidates = urls(repo, host);
|
|
41
|
+
const observation = { repo, host };
|
|
42
|
+
for (const transport of ['https', 'ssh']) {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await exec(
|
|
45
|
+
'git',
|
|
46
|
+
['ls-remote', '--symref', candidates[transport], 'HEAD'],
|
|
47
|
+
{ shell: false, encoding: 'utf8', timeout: 30_000 },
|
|
48
|
+
);
|
|
49
|
+
observation[transport] = {
|
|
50
|
+
status: 'available',
|
|
51
|
+
identity: identity(stdout),
|
|
52
|
+
};
|
|
53
|
+
} catch (error) {
|
|
54
|
+
observation[transport] = {
|
|
55
|
+
status: 'unavailable',
|
|
56
|
+
error: error.message,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (observation.https.status === 'unavailable' && observation.ssh.status === 'unavailable') {
|
|
61
|
+
throw new ReleaseError(
|
|
62
|
+
REMOTE_UNAVAILABLE,
|
|
63
|
+
`neither HTTPS nor SSH can read ${repo}`,
|
|
64
|
+
{ repository: repo, transports: observation },
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
observation.https.status === 'available'
|
|
69
|
+
&& observation.ssh.status === 'available'
|
|
70
|
+
&& observation.https.identity !== observation.ssh.identity
|
|
71
|
+
) {
|
|
72
|
+
throw new ReleaseError(
|
|
73
|
+
REMOTE_CONFLICT,
|
|
74
|
+
`HTTPS and SSH resolve different remote identities for ${repo}`,
|
|
75
|
+
{ repository: repo },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
observations.push(observation);
|
|
79
|
+
}
|
|
80
|
+
const transport = observations.some((entry) => entry.https.status === 'unavailable')
|
|
81
|
+
? 'ssh'
|
|
82
|
+
: 'https';
|
|
83
|
+
if (
|
|
84
|
+
transport === 'ssh'
|
|
85
|
+
&& observations.some((entry) => entry.ssh.status !== 'available')
|
|
86
|
+
) {
|
|
87
|
+
throw new ReleaseError(
|
|
88
|
+
REMOTE_UNAVAILABLE,
|
|
89
|
+
'no single safe Git transport is available for every release repository',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { transport, repositories: observations };
|
|
93
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
import { ReleaseError, CONFIG_INVALID, GATE_FAILED } from './errors.mjs';
|
|
6
|
+
|
|
7
|
+
function assertOid(value, label) {
|
|
8
|
+
if (typeof value !== 'string' || !/^[a-f0-9]{40,64}$/.test(value)) {
|
|
9
|
+
throw new ReleaseError(GATE_FAILED, `${label} is not a full Git object id`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* After VERIFIED, advance the project-only previous-public baseline to the
|
|
15
|
+
* exact public commit already frozen, published and verified. This prepares
|
|
16
|
+
* the next release without asking maintainers to copy commits from terminal
|
|
17
|
+
* output. It never edits public files or performs remote writes.
|
|
18
|
+
*/
|
|
19
|
+
export async function updatePreviousPublicBaselines(options = {}) {
|
|
20
|
+
const root = resolve(options.root ?? process.cwd());
|
|
21
|
+
const planPath = resolve(options.planPath ?? '');
|
|
22
|
+
const configPath = resolve(root, '.release-skill', 'project.yaml');
|
|
23
|
+
let plan;
|
|
24
|
+
let configRaw;
|
|
25
|
+
let configStat;
|
|
26
|
+
try {
|
|
27
|
+
[plan, configRaw, configStat] = await Promise.all([
|
|
28
|
+
readFile(planPath, 'utf8').then(JSON.parse),
|
|
29
|
+
readFile(configPath, 'utf8'),
|
|
30
|
+
lstat(configPath),
|
|
31
|
+
]);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new ReleaseError(
|
|
34
|
+
CONFIG_INVALID,
|
|
35
|
+
`cannot load release metadata authorities: ${error.message}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!configStat.isFile() || configStat.isSymbolicLink()) {
|
|
39
|
+
throw new ReleaseError(CONFIG_INVALID, 'project config must be a regular non-symlink file');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const doc = YAML.parseDocument(configRaw, { uniqueKeys: true });
|
|
43
|
+
if (doc.errors.length > 0) {
|
|
44
|
+
throw new ReleaseError(CONFIG_INVALID, `project config YAML is invalid: ${doc.errors[0].message}`);
|
|
45
|
+
}
|
|
46
|
+
const releaseUnits = doc.get('releaseUnits', true);
|
|
47
|
+
if (!YAML.isSeq(releaseUnits)) {
|
|
48
|
+
throw new ReleaseError(CONFIG_INVALID, 'project config releaseUnits must be a sequence');
|
|
49
|
+
}
|
|
50
|
+
const configUnits = new Map();
|
|
51
|
+
for (const node of releaseUnits.items) {
|
|
52
|
+
const id = node?.get?.('id');
|
|
53
|
+
if (typeof id === 'string') configUnits.set(id, node);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const updates = [];
|
|
57
|
+
for (const unit of plan.units ?? []) {
|
|
58
|
+
const configUnit = configUnits.get(unit.id);
|
|
59
|
+
if (!configUnit) {
|
|
60
|
+
throw new ReleaseError(CONFIG_INVALID, `frozen unit "${unit.id}" is absent from project config`);
|
|
61
|
+
}
|
|
62
|
+
const frozen = unit.frozenSnapshot;
|
|
63
|
+
assertOid(frozen?.commit, `unit "${unit.id}" frozen commit`);
|
|
64
|
+
assertOid(frozen?.tree, `unit "${unit.id}" frozen tree`);
|
|
65
|
+
if (!/^[a-f0-9]{64}$/.test(frozen?.manifestDigest ?? '')) {
|
|
66
|
+
throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" frozen manifest digest is invalid`);
|
|
67
|
+
}
|
|
68
|
+
const old = configUnit.get('previousPublicBaseline', true)?.toJSON?.() ?? {};
|
|
69
|
+
const next = {
|
|
70
|
+
mode: 'bound',
|
|
71
|
+
repo: old.repo ?? unit.publicRepo,
|
|
72
|
+
ref: old.ref ?? `refs/heads/${frozen.branch}`,
|
|
73
|
+
commit: frozen.commit,
|
|
74
|
+
tree: frozen.tree,
|
|
75
|
+
manifestDigest: frozen.manifestDigest,
|
|
76
|
+
};
|
|
77
|
+
if (!next.repo || !next.ref) {
|
|
78
|
+
throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" cannot derive its next public baseline identity`);
|
|
79
|
+
}
|
|
80
|
+
configUnit.set('previousPublicBaseline', next);
|
|
81
|
+
updates.push({
|
|
82
|
+
id: unit.id,
|
|
83
|
+
previousCommit: old.commit ?? null,
|
|
84
|
+
commit: frozen.commit,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const nextRaw = doc.toString();
|
|
89
|
+
if (nextRaw === configRaw) {
|
|
90
|
+
return { status: 'UNCHANGED', configPath, units: updates };
|
|
91
|
+
}
|
|
92
|
+
const tempPath = `${configPath}.${process.pid}.${Date.now()}.tmp`;
|
|
93
|
+
try {
|
|
94
|
+
await writeFile(tempPath, nextRaw, {
|
|
95
|
+
encoding: 'utf8',
|
|
96
|
+
mode: configStat.mode & 0o777,
|
|
97
|
+
flag: 'wx',
|
|
98
|
+
});
|
|
99
|
+
await rename(tempPath, configPath);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
102
|
+
throw new ReleaseError(CONFIG_INVALID, `cannot update project release metadata: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
return { status: 'UPDATED', configPath, units: updates };
|
|
105
|
+
}
|
|
@@ -275,10 +275,16 @@ export function evaluateConsumerSkillResourceClosureReceipts(plan, receipts) {
|
|
|
275
275
|
'kimi-plugin',
|
|
276
276
|
'codebuddy-plugin',
|
|
277
277
|
]);
|
|
278
|
+
// When plan declares humanConsumersStrategy: 'manualFollowUps', Kimi/CodeBuddy
|
|
279
|
+
// are non-blocking manual follow-up tasks and are not verified by the system.
|
|
280
|
+
// Exclude them from the expected receipt set.
|
|
281
|
+
const humanConsumerTypes = plan.humanConsumersStrategy === 'manualFollowUps'
|
|
282
|
+
? new Set(['kimi-plugin', 'codebuddy-plugin'])
|
|
283
|
+
: new Set();
|
|
278
284
|
const expectedKeys = [];
|
|
279
285
|
for (const unit of plan.units ?? []) {
|
|
280
286
|
for (const distribution of unit.distributions ?? []) {
|
|
281
|
-
if (supported.has(distribution.type)) {
|
|
287
|
+
if (supported.has(distribution.type) && !humanConsumerTypes.has(distribution.type)) {
|
|
282
288
|
expectedKeys.push(`${unit.id}:${distribution.type}`);
|
|
283
289
|
}
|
|
284
290
|
}
|