release-skill 0.1.1 → 0.1.4
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 +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/CHANGELOG.md +89 -0
- package/INSTALL.md +216 -5
- package/INSTALL.zh-CN.md +358 -0
- package/README.md +411 -67
- package/README.zh-CN.md +377 -59
- 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 +79284 -0
- package/adapters/claude/bin/release-skill.mjs +34 -0
- package/adapters/claude/native/safe-write/binding.gyp +40 -0
- package/adapters/claude/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/claude/native/safe-write/prebuilds.json +24 -0
- package/adapters/claude/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/claude/schemas/.render-manifest.json +37 -0
- package/adapters/claude/schemas/approval-record.schema.json +115 -0
- package/adapters/claude/schemas/artifact-lock.schema.json +111 -0
- package/adapters/claude/schemas/artifact-plan.schema.json +52 -0
- package/adapters/claude/schemas/artifact-policy.schema.json +76 -0
- package/adapters/claude/schemas/evidence-event.schema.json +89 -0
- package/adapters/claude/schemas/release-plan.schema.json +860 -0
- package/adapters/claude/schemas/release-project.schema.json +736 -0
- package/adapters/claude/schemas/release-run.schema.json +342 -0
- package/adapters/claude/skills/release-assess/SKILL.md +5 -6
- package/adapters/claude/skills/release-help/SKILL.md +14 -18
- package/adapters/claude/skills/release-prepare/SKILL.md +16 -6
- package/adapters/claude/skills/release-publish/SKILL.md +7 -7
- package/adapters/claude/skills/release-reconcile/SKILL.md +6 -6
- package/adapters/claude/skills/release-setup/SKILL.md +95 -0
- package/adapters/claude/skills/release-verify/SKILL.md +7 -7
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +79284 -0
- package/adapters/codex/bin/release-skill.mjs +34 -0
- package/adapters/codex/native/safe-write/binding.gyp +40 -0
- package/adapters/codex/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/codex/native/safe-write/prebuilds.json +24 -0
- package/adapters/codex/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/codex/schemas/.render-manifest.json +37 -0
- package/adapters/codex/schemas/approval-record.schema.json +115 -0
- package/adapters/codex/schemas/artifact-lock.schema.json +111 -0
- package/adapters/codex/schemas/artifact-plan.schema.json +52 -0
- package/adapters/codex/schemas/artifact-policy.schema.json +76 -0
- package/adapters/codex/schemas/evidence-event.schema.json +89 -0
- package/adapters/codex/schemas/release-plan.schema.json +860 -0
- package/adapters/codex/schemas/release-project.schema.json +736 -0
- package/adapters/codex/schemas/release-run.schema.json +342 -0
- package/adapters/codex/skills/release-assess/SKILL.md +12 -6
- package/adapters/codex/skills/release-help/SKILL.md +21 -18
- package/adapters/codex/skills/release-prepare/SKILL.md +23 -6
- package/adapters/codex/skills/release-publish/SKILL.md +14 -7
- package/adapters/codex/skills/release-reconcile/SKILL.md +13 -6
- package/adapters/codex/skills/release-setup/SKILL.md +102 -0
- package/adapters/codex/skills/release-verify/SKILL.md +14 -7
- package/bin/release-skill-cli.mjs +807 -0
- package/bin/release-skill.bundle.mjs +79284 -0
- package/bin/release-skill.mjs +23 -732
- package/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/native/safe-write/prebuilds.json +22 -2
- package/native/safe-write/src/safe_write.cc +11 -2
- package/package.json +8 -2
- package/references/02-project-config.md +55 -4
- package/references/05-evidence-and-errors.md +6 -2
- package/schemas/release-plan.schema.json +556 -65
- package/schemas/release-project.schema.json +406 -29
- package/schemas/release-run.schema.json +165 -18
- package/scripts/build-bundle.mjs +133 -0
- package/skills/release-assess/SKILL.md +5 -6
- package/skills/release-help/SKILL.md +14 -18
- package/skills/release-prepare/SKILL.md +16 -6
- package/skills/release-publish/SKILL.md +7 -7
- package/skills/release-reconcile/SKILL.md +6 -6
- package/skills/release-setup/SKILL.md +95 -0
- package/skills/release-verify/SKILL.md +7 -7
- package/skills-src/release-assess/SKILL.md +5 -6
- package/skills-src/release-help/SKILL.md +14 -18
- package/skills-src/release-prepare/SKILL.md +16 -6
- package/skills-src/release-publish/SKILL.md +7 -7
- package/skills-src/release-reconcile/SKILL.md +6 -6
- package/skills-src/release-setup/SKILL.md +95 -0
- package/skills-src/release-verify/SKILL.md +7 -7
- package/src/adapters/contract.mjs +3 -0
- package/src/adapters/git-github.mjs +84 -2
- package/src/adapters/npm.mjs +5 -13
- package/src/adapters/plugin-marketplace.mjs +132 -52
- package/src/adapters/push-snapshot.mjs +84 -17
- package/src/artifacts/policy.mjs +4 -7
- package/src/artifacts/safe-fs-backend-internal.mjs +69 -21
- package/src/commands/prepare.mjs +244 -20
- package/src/commands/publish.mjs +46 -0
- package/src/commands/reconcile.mjs +152 -0
- package/src/commands/setup.mjs +1525 -0
- package/src/commands/verify.mjs +122 -26
- package/src/core/approval.mjs +4 -6
- package/src/core/config.mjs +42 -8
- package/src/core/errors.mjs +4 -0
- package/src/core/pkg-root.mjs +22 -0
- package/src/core/plan.mjs +132 -4
- package/src/core/previous-public-baseline.mjs +21 -1
- package/src/core/run.mjs +4 -4
- package/src/core/trusted-resource.mjs +96 -0
- package/src/core/verification-gates.mjs +451 -0
- package/src/docs/version-gate.mjs +164 -0
- package/src/producers/build-adapters.mjs +512 -55
- package/src/snapshot/frozen.mjs +221 -7
- package/src/snapshot/public-map.mjs +7 -4
- package/src/snapshot/scan.mjs +2 -1
package/src/adapters/npm.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFile as execFileCb, spawn } from 'node:child_process';
|
|
3
3
|
import { gunzipSync } from 'node:zlib';
|
|
4
|
-
import { createRequire } from 'node:module';
|
|
5
4
|
import { promisify } from 'node:util';
|
|
6
5
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
7
6
|
import { constants as fsConstants } from 'node:fs';
|
|
8
7
|
import { chmod, lstat, mkdtemp, open, readFile, realpath, rm } from 'node:fs/promises';
|
|
9
8
|
|
|
9
|
+
import libnpmpublish from 'libnpmpublish';
|
|
10
|
+
import npmRegistryFetch from 'npm-registry-fetch';
|
|
11
|
+
|
|
10
12
|
import {
|
|
11
13
|
ActionStatus,
|
|
12
14
|
ActionType,
|
|
@@ -67,20 +69,14 @@ async function run(command, args, options = {}) {
|
|
|
67
69
|
* Authentication and registry options must be supplied explicitly by the
|
|
68
70
|
* caller; errors are sanitized before they cross the adapter boundary.
|
|
69
71
|
*/
|
|
70
|
-
let _libnpmpublish;
|
|
71
|
-
let _npmRegistryFetch;
|
|
72
72
|
async function defaultPublishTarballBuffer({ buffer, manifest, opts: publishOpts }) {
|
|
73
|
-
if (!_libnpmpublish) {
|
|
74
|
-
const require = createRequire(import.meta.url);
|
|
75
|
-
_libnpmpublish = require('libnpmpublish');
|
|
76
|
-
}
|
|
77
73
|
const libOpts = {};
|
|
78
74
|
if (publishOpts.registry) libOpts.registry = publishOpts.registry;
|
|
79
75
|
if (publishOpts.token) libOpts.forceAuth = { token: publishOpts.token };
|
|
80
76
|
if (publishOpts.access) libOpts.access = publishOpts.access;
|
|
81
77
|
if (publishOpts.tag) libOpts.defaultTag = publishOpts.tag;
|
|
82
78
|
if (publishOpts.provenance) libOpts.provenance = publishOpts.provenance;
|
|
83
|
-
return
|
|
79
|
+
return libnpmpublish.publish(manifest, buffer, libOpts);
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
export function registryTokenKey(registry) {
|
|
@@ -144,11 +140,7 @@ export async function resolveNpmRegistryAuthToken(options) {
|
|
|
144
140
|
|
|
145
141
|
async function defaultWhoamiWithToken({ registry, token, cwd, exec }) {
|
|
146
142
|
try {
|
|
147
|
-
|
|
148
|
-
const require = createRequire(import.meta.url);
|
|
149
|
-
_npmRegistryFetch = require('npm-registry-fetch');
|
|
150
|
-
}
|
|
151
|
-
const result = await _npmRegistryFetch.json('/-/whoami', {
|
|
143
|
+
const result = await npmRegistryFetch.json('/-/whoami', {
|
|
152
144
|
registry: `${normalizeRegistry(registry)}/`,
|
|
153
145
|
forceAuth: { token },
|
|
154
146
|
preferOnline: true,
|
|
@@ -33,6 +33,43 @@ const execFile = promisify(execFileCb);
|
|
|
33
33
|
|
|
34
34
|
const NAME = 'plugin-marketplace';
|
|
35
35
|
|
|
36
|
+
function transportPayload(entries) {
|
|
37
|
+
return entries.map(({ path, type, mode, size, contentDigest }) => ({
|
|
38
|
+
path,
|
|
39
|
+
type,
|
|
40
|
+
// The local authority removes write bits when sealing. Git checkout and
|
|
41
|
+
// plugin installation restore owner-write permission, while preserving
|
|
42
|
+
// executable intent. Ignore only write bits; retain every other mode bit.
|
|
43
|
+
mode: mode & ~0o222,
|
|
44
|
+
size,
|
|
45
|
+
contentDigest,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
|
|
50
|
+
const sourcePath = await resolveFrozenPath(
|
|
51
|
+
context.root,
|
|
52
|
+
action.snapshotPath,
|
|
53
|
+
'frozen marketplace snapshot',
|
|
54
|
+
);
|
|
55
|
+
const sourceSnapshot = await computeFrozenSnapshot(sourcePath);
|
|
56
|
+
if (sourceSnapshot.digest !== action.manifestDigest) {
|
|
57
|
+
throw new Error('frozen marketplace snapshot digest no longer matches the plan');
|
|
58
|
+
}
|
|
59
|
+
const installedSnapshot = await computeFrozenSnapshot(installPath, {
|
|
60
|
+
excludeRootEntries: consumer === 'codex' ? ['.git'] : [],
|
|
61
|
+
});
|
|
62
|
+
if (
|
|
63
|
+
JSON.stringify(transportPayload(sourceSnapshot.entries))
|
|
64
|
+
!== JSON.stringify(transportPayload(installedSnapshot.entries))
|
|
65
|
+
) {
|
|
66
|
+
throw new Error('installed marketplace payload differs in path, bytes, size, or non-write mode bits');
|
|
67
|
+
}
|
|
68
|
+
// This is not an expected-value backfill: the sealed authority digest was
|
|
69
|
+
// revalidated above and the installed payload was independently compared.
|
|
70
|
+
return action.manifestDigest;
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
async function writeEvidenceAtomic(filePath, value) {
|
|
37
74
|
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
38
75
|
try {
|
|
@@ -386,26 +423,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
386
423
|
});
|
|
387
424
|
}
|
|
388
425
|
|
|
389
|
-
// Verify marketplace files exist
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
: '.codex-plugin/plugin.json';
|
|
426
|
+
// Verify marketplace files exist.
|
|
427
|
+
// marketplace.json is at the snapshot root; plugin manifest is
|
|
428
|
+
// resolved relative to the entry's declared source path.
|
|
393
429
|
const marketplaceRelative = consumer === 'claude'
|
|
394
430
|
? '.claude-plugin/marketplace.json'
|
|
395
431
|
: '.agents/plugins/marketplace.json';
|
|
396
432
|
|
|
397
|
-
const manifestPath = resolve(snapshotDirReal, manifestRelative);
|
|
398
433
|
const marketplacePath = resolve(snapshotDirReal, marketplaceRelative);
|
|
399
434
|
|
|
400
|
-
const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
|
|
401
|
-
if (!manifestResult.valid) {
|
|
402
|
-
return createResult({
|
|
403
|
-
actionType,
|
|
404
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
405
|
-
error: `frozen snapshot ${manifestRelative} invalid: ${manifestResult.error}`,
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
|
|
409
435
|
// marketplace.json must exist and have root name (no root version required)
|
|
410
436
|
const marketplaceResult = await validateManifestFile(marketplacePath, ['name']);
|
|
411
437
|
if (!marketplaceResult.valid) {
|
|
@@ -444,23 +470,70 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
444
470
|
}
|
|
445
471
|
const entry = pluginEntry[0];
|
|
446
472
|
|
|
447
|
-
// Entry source must be
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
473
|
+
// Entry source must be a safe relative path within the snapshot.
|
|
474
|
+
// Accepts "./" (root-level), "./adapters/claude" (subdirectory),
|
|
475
|
+
// etc. Rejects absolute paths, ".." traversal, remote URLs, and
|
|
476
|
+
// empty strings.
|
|
477
|
+
const sourcePath = consumer === 'claude'
|
|
478
|
+
? entry.source
|
|
479
|
+
: entry.source?.source === 'local' ? entry.source?.path : null;
|
|
480
|
+
if (typeof sourcePath !== 'string' || sourcePath.length === 0) {
|
|
481
|
+
return createResult({
|
|
482
|
+
actionType,
|
|
483
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
484
|
+
error: `marketplace plugin entry source must be a non-empty relative path${consumer === 'codex' ? ' (object with source:"local")' : ''}, got ${JSON.stringify(entry.source)}`,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (
|
|
488
|
+
sourcePath.startsWith('/') ||
|
|
489
|
+
sourcePath.includes('..') ||
|
|
490
|
+
sourcePath.includes('\\') ||
|
|
491
|
+
/^https?:\/\//i.test(sourcePath)
|
|
492
|
+
) {
|
|
493
|
+
return createResult({
|
|
494
|
+
actionType,
|
|
495
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
496
|
+
error: `marketplace plugin entry source "${sourcePath}" is not a safe relative path`,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
// Verify the declared source directory exists and contains the
|
|
500
|
+
// expected plugin manifest inside the frozen snapshot.
|
|
501
|
+
const sourceDirAbs = resolve(snapshotDirReal, sourcePath);
|
|
502
|
+
const sourceDirReal = await realpath(sourceDirAbs).catch(() => null);
|
|
503
|
+
if (!sourceDirReal) {
|
|
504
|
+
return createResult({
|
|
505
|
+
actionType,
|
|
506
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
507
|
+
error: `marketplace plugin entry source directory does not exist: ${sourcePath}`,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
// Containment check: source must stay inside the snapshot
|
|
511
|
+
const sourceRelCheck = relative(snapshotDirReal, sourceDirReal);
|
|
512
|
+
if (sourceRelCheck.startsWith('..') || isAbsolute(sourceRelCheck)) {
|
|
513
|
+
return createResult({
|
|
514
|
+
actionType,
|
|
515
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
516
|
+
error: `marketplace plugin entry source "${sourcePath}" escapes the frozen snapshot`,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Resolve plugin manifest relative to the declared source path.
|
|
521
|
+
// For root layouts (source: "./"), this resolves to
|
|
522
|
+
// snapshot/.claude-plugin/plugin.json
|
|
523
|
+
// For subdirectory layouts (source: "./adapters/claude"), this resolves to
|
|
524
|
+
// snapshot/adapters/claude/.claude-plugin/plugin.json
|
|
525
|
+
const manifestRelative = consumer === 'claude'
|
|
526
|
+
? join(sourcePath, '.claude-plugin', 'plugin.json')
|
|
527
|
+
: join(sourcePath, '.codex-plugin', 'plugin.json');
|
|
528
|
+
const manifestPath = resolve(snapshotDirReal, manifestRelative);
|
|
529
|
+
|
|
530
|
+
const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
|
|
531
|
+
if (!manifestResult.valid) {
|
|
532
|
+
return createResult({
|
|
533
|
+
actionType,
|
|
534
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
535
|
+
error: `frozen snapshot ${manifestRelative} invalid: ${manifestResult.error}`,
|
|
536
|
+
});
|
|
464
537
|
}
|
|
465
538
|
|
|
466
539
|
// Claude carries the version in the marketplace entry. Codex keeps
|
|
@@ -862,10 +935,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
862
935
|
let executeManifestDigest = null;
|
|
863
936
|
if (installPath) {
|
|
864
937
|
try {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
938
|
+
executeManifestDigest = await verifyInstalledMarketplacePayload(
|
|
939
|
+
action,
|
|
940
|
+
context,
|
|
941
|
+
installPath,
|
|
942
|
+
consumer,
|
|
943
|
+
);
|
|
869
944
|
} catch {
|
|
870
945
|
// Digest computation failure is caught at verify time
|
|
871
946
|
}
|
|
@@ -1176,26 +1251,30 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1176
1251
|
});
|
|
1177
1252
|
}
|
|
1178
1253
|
|
|
1179
|
-
//
|
|
1254
|
+
// Bind the installed payload back to the sealed authority while
|
|
1255
|
+
// normalizing only transport-restored write permission bits.
|
|
1180
1256
|
let manifestDigest;
|
|
1257
|
+
let manifestError = null;
|
|
1181
1258
|
try {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1259
|
+
manifestDigest = await verifyInstalledMarketplacePayload(
|
|
1260
|
+
action,
|
|
1261
|
+
context,
|
|
1262
|
+
installPath,
|
|
1263
|
+
consumer,
|
|
1264
|
+
);
|
|
1188
1265
|
} catch (digestErr) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1266
|
+
// Preserve independently observed fields for diagnostics. This
|
|
1267
|
+
// raw digest is not accepted as plan authority because the error
|
|
1268
|
+
// is returned and verify therefore fails closed.
|
|
1269
|
+
try {
|
|
1270
|
+
const installedSnapshot = await computeFrozenSnapshot(installPath, {
|
|
1271
|
+
excludeRootEntries: consumer === 'codex' ? ['.git'] : [],
|
|
1272
|
+
});
|
|
1273
|
+
manifestDigest = installedSnapshot.digest;
|
|
1274
|
+
} catch {
|
|
1275
|
+
manifestDigest = undefined;
|
|
1276
|
+
}
|
|
1277
|
+
manifestError = `failed to bind manifestDigest to frozen authority: ${digestErr.message}`;
|
|
1199
1278
|
}
|
|
1200
1279
|
|
|
1201
1280
|
// Build observation with CLI-proven fields only (no action backfill)
|
|
@@ -1318,6 +1397,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1318
1397
|
actionType,
|
|
1319
1398
|
status: ActionStatus.OBSERVED,
|
|
1320
1399
|
observation,
|
|
1400
|
+
error: manifestError,
|
|
1321
1401
|
});
|
|
1322
1402
|
}
|
|
1323
1403
|
|
|
@@ -54,6 +54,37 @@ function validateAction(action) {
|
|
|
54
54
|
validateOid(action.commit, 'commit');
|
|
55
55
|
validateOid(action.tree, 'tree');
|
|
56
56
|
githubRepositoryUrl(action.repo, action.githubHost);
|
|
57
|
+
|
|
58
|
+
const validStrategies = ['create-release-branch', 'advance-existing-branch', 'initialize-default-branch'];
|
|
59
|
+
const strategy = action.branchStrategy ?? 'create-release-branch';
|
|
60
|
+
if (typeof strategy !== 'string' || !validStrategies.includes(strategy)) {
|
|
61
|
+
throw new Error(`branchStrategy must be one of: ${validStrategies.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
if (strategy === 'advance-existing-branch' || strategy === 'initialize-default-branch') {
|
|
64
|
+
if (!action.parentCommit || typeof action.parentCommit !== 'string') {
|
|
65
|
+
throw new Error(`${strategy} requires parentCommit`);
|
|
66
|
+
}
|
|
67
|
+
validateOid(action.parentCommit, 'parentCommit');
|
|
68
|
+
}
|
|
69
|
+
if (strategy === 'advance-existing-branch') {
|
|
70
|
+
if (!action.expectedBaselineCommit || typeof action.expectedBaselineCommit !== 'string') {
|
|
71
|
+
throw new Error('advance-existing-branch requires expectedBaselineCommit');
|
|
72
|
+
}
|
|
73
|
+
validateOid(action.expectedBaselineCommit, 'expectedBaselineCommit');
|
|
74
|
+
if (action.expectedBaselineCommit !== action.parentCommit) {
|
|
75
|
+
throw new Error('advance-existing-branch expectedBaselineCommit must equal parentCommit');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (strategy === 'create-release-branch' && (action.parentCommit || action.expectedBaselineCommit)) {
|
|
79
|
+
throw new Error('create-release-branch must not declare parentCommit or expectedBaselineCommit');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (action.parentCommit != null && typeof action.parentCommit === 'string') {
|
|
83
|
+
validateOid(action.parentCommit, 'parentCommit');
|
|
84
|
+
}
|
|
85
|
+
if (action.expectedBaselineCommit != null && typeof action.expectedBaselineCommit === 'string') {
|
|
86
|
+
validateOid(action.expectedBaselineCommit, 'expectedBaselineCommit');
|
|
87
|
+
}
|
|
57
88
|
}
|
|
58
89
|
|
|
59
90
|
async function inspectLocalObjects(action, context, exec) {
|
|
@@ -62,6 +93,7 @@ async function inspectLocalObjects(action, context, exec) {
|
|
|
62
93
|
gitObjectDir: action.gitObjectDir,
|
|
63
94
|
commit: action.commit,
|
|
64
95
|
tree: action.tree,
|
|
96
|
+
parentCommit: action.parentCommit,
|
|
65
97
|
exec,
|
|
66
98
|
});
|
|
67
99
|
await exec('git', ['check-ref-format', `refs/heads/${action.branch}`], { shell: false });
|
|
@@ -91,12 +123,32 @@ export function createPushSnapshotAdapter(deps = {}) {
|
|
|
91
123
|
['ls-remote', '--heads', remoteUrl, `refs/heads/${action.branch}`],
|
|
92
124
|
{ shell: false },
|
|
93
125
|
);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
126
|
+
const remoteTip = stdout.trim().split(/\s+/)[0] ?? '';
|
|
127
|
+
const strategy = action.branchStrategy ?? 'create-release-branch';
|
|
128
|
+
|
|
129
|
+
if (strategy === 'advance-existing-branch') {
|
|
130
|
+
if (!remoteTip) {
|
|
131
|
+
return createResult({
|
|
132
|
+
actionType: action.actionType,
|
|
133
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
134
|
+
error: `advance-existing-branch requires remote branch to exist: ${action.branch}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (remoteTip !== action.expectedBaselineCommit) {
|
|
138
|
+
return createResult({
|
|
139
|
+
actionType: action.actionType,
|
|
140
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
141
|
+
error: `remote branch tip mismatch: expected ${action.expectedBaselineCommit} but found ${remoteTip}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} else if (strategy === 'initialize-default-branch' || strategy === 'create-release-branch') {
|
|
145
|
+
if (remoteTip) {
|
|
146
|
+
return createResult({
|
|
147
|
+
actionType: action.actionType,
|
|
148
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
149
|
+
error: `remote branch already exists: ${action.branch}; human intervention required`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
100
152
|
}
|
|
101
153
|
return createResult({ actionType: action.actionType, status: ActionStatus.PREFLIGHT_PASSED });
|
|
102
154
|
} catch (err) {
|
|
@@ -119,17 +171,32 @@ export function createPushSnapshotAdapter(deps = {}) {
|
|
|
119
171
|
});
|
|
120
172
|
const gitDir = await inspectLocalObjects(action, context, exec);
|
|
121
173
|
const remoteUrl = githubRepositoryUrl(action.repo, action.githubHost);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
'
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
174
|
+
const strategy = action.branchStrategy ?? 'create-release-branch';
|
|
175
|
+
|
|
176
|
+
if (strategy === 'advance-existing-branch') {
|
|
177
|
+
await exec(
|
|
178
|
+
'git',
|
|
179
|
+
[
|
|
180
|
+
'--git-dir', gitDir,
|
|
181
|
+
'push',
|
|
182
|
+
remoteUrl,
|
|
183
|
+
`${action.commit}:refs/heads/${action.branch}`,
|
|
184
|
+
],
|
|
185
|
+
{ shell: false },
|
|
186
|
+
);
|
|
187
|
+
} else {
|
|
188
|
+
await exec(
|
|
189
|
+
'git',
|
|
190
|
+
[
|
|
191
|
+
'--git-dir', gitDir,
|
|
192
|
+
'push',
|
|
193
|
+
`--force-with-lease=refs/heads/${action.branch}:`,
|
|
194
|
+
remoteUrl,
|
|
195
|
+
`${action.commit}:refs/heads/${action.branch}`,
|
|
196
|
+
],
|
|
197
|
+
{ shell: false },
|
|
198
|
+
);
|
|
199
|
+
}
|
|
133
200
|
return createResult({ actionType: action.actionType, status: ActionStatus.EXECUTED });
|
|
134
201
|
} catch (err) {
|
|
135
202
|
return createResult({
|
package/src/artifacts/policy.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
import { createHash } from 'node:crypto';
|
|
16
15
|
import YAML from 'yaml';
|
|
@@ -22,21 +21,19 @@ import {
|
|
|
22
21
|
PATH_UNSAFE,
|
|
23
22
|
} from '../core/errors.mjs';
|
|
24
23
|
import { canonicalArtifactPath } from './path-key.mjs';
|
|
24
|
+
import { readTrustedPackageResourceSync } from '../core/trusted-resource.mjs';
|
|
25
25
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
// Schema validation
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
|
|
30
|
-
const POLICY_SCHEMA_PATH = new URL(
|
|
31
|
-
'../../schemas/artifact-policy.schema.json',
|
|
32
|
-
import.meta.url,
|
|
33
|
-
);
|
|
34
|
-
|
|
35
30
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
36
31
|
addFormats(ajv);
|
|
37
32
|
|
|
38
33
|
// Compile schema once at module load (synchronous).
|
|
39
|
-
const _validate = ajv.compile(JSON.parse(
|
|
34
|
+
const _validate = ajv.compile(JSON.parse(readTrustedPackageResourceSync(
|
|
35
|
+
'schemas/artifact-policy.schema.json',
|
|
36
|
+
).toString('utf8')));
|
|
40
37
|
|
|
41
38
|
/**
|
|
42
39
|
* Validate a policy object against the artifact-policy JSON Schema.
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import { realpath } from 'node:fs/promises';
|
|
14
15
|
import { readFileSync, realpathSync, lstatSync } from 'node:fs';
|
|
15
16
|
import { createHash } from 'node:crypto';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
ReleaseError,
|
|
@@ -25,17 +25,71 @@ import {
|
|
|
25
25
|
// Constants
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
28
|
+
// Keep this internal loader self-contained. Besides making the native trust
|
|
29
|
+
// boundary auditable, this lets isolated consumers test/copy it together with
|
|
30
|
+
// errors.mjs only. In the bundle the banner supplies __bundlePkgRoot; in source
|
|
31
|
+
// mode the module's own URL deterministically identifies the package root.
|
|
32
|
+
const PKG_ROOT = typeof __bundlePkgRoot !== 'undefined'
|
|
33
|
+
? __bundlePkgRoot
|
|
34
|
+
: resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
35
|
+
const NATIVE_BASE = resolve(PKG_ROOT, 'native/safe-write');
|
|
36
|
+
const PREBUILDS_PATH = resolve(NATIVE_BASE, 'prebuilds.json');
|
|
37
|
+
const PREBUILDS_RESOURCE = 'native/safe-write/prebuilds.json';
|
|
38
|
+
const DEFAULT_ADDON_PATH = resolve(NATIVE_BASE, 'build/Release/safe_write.node');
|
|
33
39
|
|
|
34
40
|
const EXPECTED_EXPORTS = Object.freeze([
|
|
35
41
|
'openRoot', 'openDir', 'readEntry', 'readFile',
|
|
36
42
|
'createTemp', 'rename', 'abortTemp', 'mkdir', 'rmdir', 'unlink', 'chmod', 'fsync', 'close',
|
|
37
43
|
]);
|
|
38
44
|
|
|
45
|
+
function failPrebuildManifest(reason, message = 'prebuilds manifest not readable') {
|
|
46
|
+
throw new ReleaseError(
|
|
47
|
+
SAFE_WRITE_UNAVAILABLE,
|
|
48
|
+
message,
|
|
49
|
+
{ resource: PREBUILDS_RESOURCE, reason },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readTrustedPrebuildManifest() {
|
|
54
|
+
let lexicalStat;
|
|
55
|
+
try {
|
|
56
|
+
lexicalStat = lstatSync(PREBUILDS_PATH, { bigint: true });
|
|
57
|
+
} catch {
|
|
58
|
+
failPrebuildManifest('MISSING');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (lexicalStat.isSymbolicLink()) {
|
|
62
|
+
failPrebuildManifest('SYMLINK', 'prebuilds manifest must not be a symlink');
|
|
63
|
+
}
|
|
64
|
+
if (!lexicalStat.isFile()) {
|
|
65
|
+
failPrebuildManifest('NOT_REGULAR_FILE', 'prebuilds manifest is not a regular file');
|
|
66
|
+
}
|
|
67
|
+
if (lexicalStat.nlink !== 1n) {
|
|
68
|
+
failPrebuildManifest('UNEXPECTED_HARDLINK_COUNT', 'prebuilds manifest has invalid nlink count');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let physicalBase;
|
|
72
|
+
let physicalManifest;
|
|
73
|
+
try {
|
|
74
|
+
physicalBase = realpathSync(NATIVE_BASE);
|
|
75
|
+
physicalManifest = realpathSync(PREBUILDS_PATH);
|
|
76
|
+
} catch {
|
|
77
|
+
failPrebuildManifest('REALPATH_FAILED');
|
|
78
|
+
}
|
|
79
|
+
if (!physicalManifest.startsWith(physicalBase + '/') && physicalManifest !== physicalBase) {
|
|
80
|
+
failPrebuildManifest(
|
|
81
|
+
'PHYSICAL_PATH_ESCAPE',
|
|
82
|
+
'prebuilds manifest physical path escapes base directory',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(readFileSync(physicalManifest, 'utf8'));
|
|
88
|
+
} catch {
|
|
89
|
+
failPrebuildManifest('READ_FAILED');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
39
93
|
// ---------------------------------------------------------------------------
|
|
40
94
|
// Error sanitization
|
|
41
95
|
// ---------------------------------------------------------------------------
|
|
@@ -58,7 +112,7 @@ function sanitizeError(err) {
|
|
|
58
112
|
* Returns the verified physical path on success, or null on failure.
|
|
59
113
|
*/
|
|
60
114
|
function isWithinPackageBuildDir(addonPath) {
|
|
61
|
-
const buildDir = resolve(
|
|
115
|
+
const buildDir = resolve(NATIVE_BASE, 'build/');
|
|
62
116
|
const resolved = resolve(addonPath);
|
|
63
117
|
if (!resolved.startsWith(buildDir + '/') && resolved !== buildDir) {
|
|
64
118
|
return null;
|
|
@@ -106,8 +160,8 @@ function validateManifestEntry(entry, key) {
|
|
|
106
160
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing exports for ${key}`);
|
|
107
161
|
}
|
|
108
162
|
|
|
109
|
-
const resolved = resolve(
|
|
110
|
-
const baseDir =
|
|
163
|
+
const resolved = resolve(NATIVE_BASE, entry.path);
|
|
164
|
+
const baseDir = NATIVE_BASE;
|
|
111
165
|
if (!resolved.startsWith(baseDir + '/') && resolved !== baseDir) {
|
|
112
166
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest path escapes base directory for ${key}`);
|
|
113
167
|
}
|
|
@@ -126,7 +180,8 @@ function validateManifestEntry(entry, key) {
|
|
|
126
180
|
|
|
127
181
|
function requireAddon(addonPath) {
|
|
128
182
|
try {
|
|
129
|
-
|
|
183
|
+
// Use PKG_ROOT-relative resolution so bundled mode can find the addon.
|
|
184
|
+
const require = createRequire(resolve(PKG_ROOT, 'package.json'));
|
|
130
185
|
return require(addonPath);
|
|
131
186
|
} catch (err) {
|
|
132
187
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, sanitizeError(err));
|
|
@@ -149,14 +204,7 @@ function requireAddon(addonPath) {
|
|
|
149
204
|
|
|
150
205
|
export function readStableAddon(options = {}) {
|
|
151
206
|
const hooks = options._hooks || {};
|
|
152
|
-
|
|
153
|
-
const prebuildsPath = resolve(__dirname, PREBUILDS_REL);
|
|
154
|
-
let manifest;
|
|
155
|
-
try {
|
|
156
|
-
manifest = JSON.parse(readFileSync(prebuildsPath, 'utf8'));
|
|
157
|
-
} catch {
|
|
158
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilds manifest not readable');
|
|
159
|
-
}
|
|
207
|
+
const manifest = readTrustedPrebuildManifest();
|
|
160
208
|
|
|
161
209
|
if (!manifest.prebuilds || typeof manifest.prebuilds !== 'object') {
|
|
162
210
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilds manifest has invalid structure');
|
|
@@ -174,7 +222,7 @@ export function readStableAddon(options = {}) {
|
|
|
174
222
|
validateManifestEntry(entry, key);
|
|
175
223
|
|
|
176
224
|
// Resolve lexical candidate path
|
|
177
|
-
const addonFile = resolve(
|
|
225
|
+
const addonFile = resolve(NATIVE_BASE, entry.path);
|
|
178
226
|
|
|
179
227
|
// Step 1: lstat LEXICAL path FIRST — reject symlink, non-regular, nlink!=1
|
|
180
228
|
// Must lstat the original path to detect symlinks; lstat on a resolved path
|
|
@@ -207,7 +255,7 @@ export function readStableAddon(options = {}) {
|
|
|
207
255
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilt addon not accessible');
|
|
208
256
|
}
|
|
209
257
|
|
|
210
|
-
const nativeBaseDir = realpathSync(
|
|
258
|
+
const nativeBaseDir = realpathSync(NATIVE_BASE);
|
|
211
259
|
if (!physicalAddonFile.startsWith(nativeBaseDir + '/') && physicalAddonFile !== nativeBaseDir) {
|
|
212
260
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilt addon physical path escapes base directory');
|
|
213
261
|
}
|
|
@@ -305,7 +353,7 @@ export function loadNativeAddon(addonPath) {
|
|
|
305
353
|
|
|
306
354
|
// 2. Development mode
|
|
307
355
|
if (process.env.RELEASE_SKILL_NATIVE_DEV === '1') {
|
|
308
|
-
const devPath =
|
|
356
|
+
const devPath = DEFAULT_ADDON_PATH;
|
|
309
357
|
const physicalPath = isWithinPackageBuildDir(devPath);
|
|
310
358
|
if (!physicalPath) {
|
|
311
359
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'dev addon path is outside build directory');
|