@cratis/pi 7.104.0 → 7.105.1
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/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +55 -7
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +80 -12
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +14 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/train-mode.md +16 -0
- package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +1017 -58
- package/package.json +1 -1
|
@@ -55,7 +55,7 @@ Commands (COMMAND --help shows these instructions too):
|
|
|
55
55
|
consumer resolves must be STAMP (none resolved: exit 1).
|
|
56
56
|
verify-pack --composition FILE [--json]
|
|
57
57
|
verify-assets CONSUMER-PATH --composition FILE [--json]
|
|
58
|
-
use-nuget CONSUMER --composition FILE [--pin-transitive]
|
|
58
|
+
use-nuget CONSUMER --composition FILE [--task ID] [--pin-transitive] [--private-cache]
|
|
59
59
|
Opt-in mixed-version NuGet closure: tab-separated producer NAME STAMP
|
|
60
60
|
ABSOLUTE_WORKTREE rows, then package PRODUCER PACKAGE_ID rows. Case-insensitive
|
|
61
61
|
IDs have one owner. All declared artifacts must exist; nothing is built/repacked.
|
|
@@ -66,25 +66,34 @@ Commands (COMMAND --help shows these instructions too):
|
|
|
66
66
|
The next mutation recovers interrupted work, or refuses and preserves unknown
|
|
67
67
|
ownership/drift. Active compositions retain member artifacts. Restore before
|
|
68
68
|
changing a composition or switching between it and a legacy overlay.
|
|
69
|
-
|
|
69
|
+
list [--json] [--stamp STAMP]
|
|
70
|
+
Read-only inventory of feed artifacts and stamp records, including legacy stamps.
|
|
71
|
+
Shows producer, task state, live/total consumers and age. Requires node; npm
|
|
72
|
+
package names are read from tarballs with tar. Takes no session lock.
|
|
73
|
+
finish --task ID [--apply]
|
|
74
|
+
Print records naming ID; --apply marks that task finished. No match is success.
|
|
75
|
+
Finishing never restores consumers or removes packages. Requires node.
|
|
76
|
+
pack-nuget PRODUCER STAMP [--task ID] [--property NAME=VALUE]... PROJECT-OR-SLN...
|
|
70
77
|
Release-pack each target into the NuGet feed; requires dotnet. Each --property
|
|
71
78
|
becomes one -p:NAME=VALUE argument of that command only (closure prints the
|
|
72
79
|
properties of each map pack row); no environment variable carries them.
|
|
73
|
-
pack-npm PRODUCER STAMP
|
|
80
|
+
pack-npm PRODUCER STAMP [--task ID]
|
|
74
81
|
Requires node, npm and Yarn 4, and a completed repository build. Temporarily
|
|
75
82
|
version non-private workspaces, pack each with npm pack --ignore-scripts (the
|
|
76
83
|
publish workflows' tool, so the tarball matches the published one) as
|
|
77
84
|
<scope>-<name>-<version>.tgz, and restore tracked manifests even on failure.
|
|
78
85
|
This helper does not build or run lifecycle scripts.
|
|
79
|
-
use-nuget CONSUMER STAMP [--pin-transitive] [PACKAGE-ID...]
|
|
86
|
+
use-nuget CONSUMER STAMP [--task ID] [--pin-transitive] [--private-cache] [PACKAGE-ID...]
|
|
80
87
|
Create a CPM overlay importing the repository's own Directory.Packages.props.
|
|
81
88
|
Transitive pinning stays as the consumer sets it; --pin-transitive forces it on so
|
|
82
89
|
stamped IDs the consumer does not pin still take effect (can raise NU1109 floors).
|
|
83
90
|
Defaults to every stamped package ID in the feed. Prints properties and sourceable
|
|
84
91
|
env. With source mapping, also prints the required restore --configfile.
|
|
92
|
+
--private-cache (or CRATIS_LOCAL_PRIVATE_CACHE=1) exports NUGET_PACKAGES in env
|
|
93
|
+
and registers a marked cache at <absolute-git-dir>/cratis-local-nuget.
|
|
85
94
|
Non-CPM: manually use an untracked project copy with explicit PackageReference
|
|
86
95
|
versions and dotnet restore --source FEED/nuget; do not edit shipped versions.
|
|
87
|
-
use-npm CONSUMER STAMP [--dir REL]
|
|
96
|
+
use-npm CONSUMER STAMP [--task ID] [--dir REL]
|
|
88
97
|
--dir REL names a Yarn project in a subdirectory (restore and clean follow it).
|
|
89
98
|
Requires node and tar. Back up package.json, yarn.lock (including existing edits),
|
|
90
99
|
and git diff; add file-tarball resolutions for every stamped feed package.
|
|
@@ -93,18 +102,37 @@ Commands (COMMAND --help shows these instructions too):
|
|
|
93
102
|
Restore npm backups when only overlay resolutions and their local lockfile
|
|
94
103
|
entries changed (including yarn install). Refuse unrelated changes and retain
|
|
95
104
|
backups for review. Remove CPM/env overlays
|
|
96
|
-
and run check. No git checkout is used on consumer files.
|
|
105
|
+
and run check. Keep the private NuGet cache. No git checkout is used on consumer files.
|
|
97
106
|
check REPO [--staged]
|
|
98
107
|
Scan added/changed tracked lines (staged-only if requested), plus all contents
|
|
99
108
|
of untracked, non-ignored files. Reports path:line:pattern and file counts.
|
|
100
|
-
clean [CONSUMER] [--stamp STAMP | --all] [--apply] [--dir REL]
|
|
109
|
+
clean [CONSUMER] [--stamp STAMP | --all | --expired [--online]] [--apply] [--dir REL] [--private-cache]
|
|
101
110
|
Default: list all local feed/cache/image/session subjects, without deleting.
|
|
102
111
|
--apply deletes listed subjects (docker rmi, never force). Refuses more than
|
|
103
112
|
CRATIS_LOCAL_CLEAN_MAX subjects (default 500), or unresolved npm backups.
|
|
104
113
|
Refuses selected stamps referenced by registered live consumer overlays/manifests;
|
|
105
114
|
restore those consumers first. Dry-run uses the same protection.
|
|
106
115
|
Known sessions are registered in FEED/sessions; an optional consumer also
|
|
107
|
-
discovers its per-worktree session.
|
|
116
|
+
discovers its per-worktree session. Registered private caches are also scanned;
|
|
117
|
+
missing/mismatched markers or unsafe cache paths refuse the entire clean (2).
|
|
118
|
+
clean CONSUMER --private-cache lists the whole marked cache for removal; --apply
|
|
119
|
+
removes it and its registration only after restore and with no live stamp.
|
|
120
|
+
Removes selected FEED/stamps/STAMP records with the artifacts on --apply.
|
|
121
|
+
--expired keeps unrecorded stamps, compositions and live references; expires
|
|
122
|
+
finished producing tasks, gone consumers or stamps superseded by a release.
|
|
123
|
+
Release evidence is offline NuGet caches unless --online opts into 10-second
|
|
124
|
+
NuGet/npm checks. Kept stamps succeed (0). --apply also prunes gone consumers
|
|
125
|
+
and missing sessions. Uses the same fuse, lock and dry-run default.
|
|
126
|
+
Docker probes have a 10-second deadline.
|
|
127
|
+
|
|
128
|
+
Pack/use commands (including --composition) accept --task ID, overriding
|
|
129
|
+
CRATIS_LOCAL_TASK, else record null. Convention: owner/repo#number. IDs match
|
|
130
|
+
^[A-Za-z0-9][A-Za-z0-9._#:/-]{0,127}$ and are JSON values, never path components.
|
|
131
|
+
Registry mutations require node. Successful packs record producer HEAD and branch;
|
|
132
|
+
use records consumers after publication; restore retains their history.
|
|
133
|
+
Invalid records: pack/use and selected clean subjects refuse with 2. Restore reads only
|
|
134
|
+
session stamps after restoring overlays; finish skips invalid records with WARNINGs.
|
|
135
|
+
Restore/finish exit 1 for invalid records; list shows invalid rows and exits 1.
|
|
108
136
|
--self-test
|
|
109
137
|
Isolated scratch HOME tests; npm uses inherited PATH to discover node.
|
|
110
138
|
Other tests use system PATH. Reports passed and skipped counts.
|
|
@@ -114,6 +142,8 @@ if present (trusted shell code), then use these defaults for unset variables:
|
|
|
114
142
|
CRATIS_LOCAL_FEED=${XDG_CACHE_HOME:-$HOME/.cache}/cratis/local-feed
|
|
115
143
|
CRATIS_LOCAL_SESSION_DIR=<absolute-git-dir>/cratis-local (per checkout)
|
|
116
144
|
NUGET_PACKAGES=$HOME/.nuget/packages
|
|
145
|
+
CRATIS_LOCAL_PRIVATE_CACHE=0 (1 opts use-nuget into a private worktree cache)
|
|
146
|
+
CRATIS_LOCAL_TASK=optional task ID (overridden by --task ID)
|
|
117
147
|
CRATIS_LOCAL_CLEAN_MAX=500
|
|
118
148
|
CRATIS_LOCAL_RUN=optional executable wrapper, invoked for each heavy step as:
|
|
119
149
|
"$CRATIS_LOCAL_RUN" build LABEL -- COMMAND ARGUMENTS...
|
|
@@ -229,7 +259,7 @@ validate_session() {
|
|
|
229
259
|
[ -e "$entry" ] || [ -L "$entry" ] || continue
|
|
230
260
|
[ ! -L "$entry" ] || die "symlink in session: $entry"
|
|
231
261
|
case "${entry##*/}" in
|
|
232
|
-
repo|stamp|env|npm-dir|Directory.Packages.props|cratis-local.targets|nuget.config) [ -f "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
262
|
+
repo|stamp|env|npm-dir|nuget-cache|Directory.Packages.props|cratis-local.targets|nuget.config) [ -f "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
233
263
|
npm-backup|composition) [ -d "$entry" ] || die "unexpected session entry: $entry" ;;
|
|
234
264
|
*) die "unrecognized file in session (preserved): $entry" ;;
|
|
235
265
|
esac
|
|
@@ -246,6 +276,50 @@ register_session() {
|
|
|
246
276
|
printf '%s\n' "$session" >> "$FEED/sessions"
|
|
247
277
|
fi
|
|
248
278
|
}
|
|
279
|
+
# Validate every helper-owned path component before trusting a cache registration.
|
|
280
|
+
validate_private_cache() {
|
|
281
|
+
local repo=$1 cache=$2 git_dir parent relative
|
|
282
|
+
git_dir=$(git -C "$repo" rev-parse --absolute-git-dir) || die "cannot find cache repository: $repo"
|
|
283
|
+
case "$cache" in "$git_dir/"*) relative=${cache#"$git_dir/"} ;; *) die "private cache outside repository git directory: $cache" ;; esac
|
|
284
|
+
case "/$relative/" in */../*|*/./*|*//*) die "unsafe private cache path: $cache" ;; esac
|
|
285
|
+
parent=$cache
|
|
286
|
+
while [ "$parent" != "$git_dir" ]; do
|
|
287
|
+
[ -d "$parent" ] && [ ! -L "$parent" ] || die "unsafe private cache directory: $parent"
|
|
288
|
+
parent=${parent%/*}
|
|
289
|
+
done
|
|
290
|
+
[ -f "$cache/.cratis-local-cache" ] && [ ! -L "$cache/.cratis-local-cache" ] || die "private cache marker missing: $cache"
|
|
291
|
+
[ "$(cat "$cache/.cratis-local-cache")" = "$repo" ] || die "private cache marker mismatch: $cache"
|
|
292
|
+
}
|
|
293
|
+
registered_private_cache() {
|
|
294
|
+
local session=$1 repo cache
|
|
295
|
+
validate_session "$session"
|
|
296
|
+
[ -f "$session/repo" ] || die "private cache session has no repository: $session"
|
|
297
|
+
repo=$(cat "$session/repo"); cache=$(cat "$session/nuget-cache")
|
|
298
|
+
[ "$(repo_root "$repo")" = "$repo" ] && [ "$(session_path "$repo")" = "$session" ] || die "private cache session no longer matches repository: $session"
|
|
299
|
+
validate_private_cache "$repo" "$cache"
|
|
300
|
+
printf '%s\n' "$cache"
|
|
301
|
+
}
|
|
302
|
+
prepare_private_cache() {
|
|
303
|
+
local repo=$1 cache=$2 session=$3
|
|
304
|
+
validate_session "$session"
|
|
305
|
+
if [ -e "$cache" ] || [ -L "$cache" ]; then validate_private_cache "$repo" "$cache"; fi
|
|
306
|
+
if [ -f "$session/nuget-cache" ]; then
|
|
307
|
+
[ "$(registered_private_cache "$session")" = "$cache" ] || die "different private cache already registered: $session"
|
|
308
|
+
fi
|
|
309
|
+
}
|
|
310
|
+
register_private_cache() {
|
|
311
|
+
local repo=$1 cache=$2 session=$3
|
|
312
|
+
prepare_private_cache "$repo" "$cache" "$session"
|
|
313
|
+
mkdir -p "$cache"
|
|
314
|
+
printf '%s\n' "$repo" > "$cache/.cratis-local-cache"
|
|
315
|
+
printf '%s\n' "$cache" > "$session/nuget-cache"
|
|
316
|
+
}
|
|
317
|
+
private_cache_restored() {
|
|
318
|
+
local session=$1 entry
|
|
319
|
+
for entry in stamp env Directory.Packages.props cratis-local.targets nuget.config npm-backup composition; do
|
|
320
|
+
[ ! -e "$session/$entry" ] && [ ! -L "$session/$entry" ] || die "private cache still referenced by $session/$entry; restore first"
|
|
321
|
+
done
|
|
322
|
+
}
|
|
249
323
|
xml_escape() { printf '%s' "$1" | sed -e 's/\&/\&/g' -e 's/</\</g' -e 's/>/\>/g' -e 's/"/\"/g' -e "s/'/\'/g"; }
|
|
250
324
|
|
|
251
325
|
# Next patch of the highest reachable vX.Y.Z tag; sets NEXT_BASE.
|
|
@@ -313,9 +387,326 @@ stamp() {
|
|
|
313
387
|
printf '%s-local.%s.g%.7s\n' "$base" "$(date -u +%Y%m%d%H%M)" "$sha"
|
|
314
388
|
}
|
|
315
389
|
|
|
390
|
+
# All stamp-record mutations use this path, after builds or overlay publication.
|
|
391
|
+
registry_update() {
|
|
392
|
+
[ "$DRY" -eq 0 ] || return 0
|
|
393
|
+
[ -n "$SESSION_LOCK" ] || acquire_session_lock
|
|
394
|
+
registry "$@"
|
|
395
|
+
}
|
|
396
|
+
registry_consumer() {
|
|
397
|
+
if ! (registry_update use "$@"); then
|
|
398
|
+
printf 'WARNING: stamp registry update failed; overlay kept; restore before cleaning\n' >&2
|
|
399
|
+
exit 1
|
|
400
|
+
fi
|
|
401
|
+
}
|
|
402
|
+
registry() {
|
|
403
|
+
need node
|
|
404
|
+
case "$1" in list|expiry) expiry_context "$@" ;; esac
|
|
405
|
+
node - "$FEED" "$TASK" "$TMP" "$@" <<'REGISTRY'
|
|
406
|
+
const fs = require('node:fs'), path = require('node:path'), cp = require('node:child_process');
|
|
407
|
+
const [feed, taskValue, scratch, operation, ...args] = process.argv.slice(2);
|
|
408
|
+
const task = taskValue || null, root = path.join(feed, 'stamps');
|
|
409
|
+
const stampPattern = /^([0-9]+\.[0-9]+\.[0-9]+)-(local\.[0-9]{12}|train)\.g[0-9a-f]{7}$/;
|
|
410
|
+
const taskPattern = /^[A-Za-z0-9][A-Za-z0-9._#:/-]{0,127}$/;
|
|
411
|
+
const now = new Date().toISOString();
|
|
412
|
+
function fail(message) { throw new Error(message); }
|
|
413
|
+
function safe(file, directory = false) {
|
|
414
|
+
const relative = path.relative(feed, file);
|
|
415
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) fail('outside stamp registry: ' + file);
|
|
416
|
+
let current = feed;
|
|
417
|
+
for (const part of ['', ...relative.split(path.sep).filter(Boolean)]) {
|
|
418
|
+
current = path.join(current, part);
|
|
419
|
+
const stat = fs.lstatSync(current, {throwIfNoEntry:false});
|
|
420
|
+
if (stat && (stat.isSymbolicLink() || (current !== file || directory ? !stat.isDirectory() : !stat.isFile()))) fail('unsafe registry path: ' + current);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function recordPath(stamp) {
|
|
424
|
+
if (!stampPattern.test(stamp)) fail('invalid registry stamp: ' + stamp);
|
|
425
|
+
const file = path.join(root, stamp, 'record.json'); safe(file); return file;
|
|
426
|
+
}
|
|
427
|
+
function read(stamp) {
|
|
428
|
+
const file = recordPath(stamp);
|
|
429
|
+
if (!fs.existsSync(file)) return null;
|
|
430
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
431
|
+
const match = stamp.match(stampPattern);
|
|
432
|
+
if (value.schema !== 1 || value.stamp !== stamp || value.base !== match[1] || value.kind !== (match[2] === 'train' ? 'train' : 'local') ||
|
|
433
|
+
!Array.isArray(value.packs) || !Array.isArray(value.consumers) || !value.tasks || typeof value.tasks !== 'object' || Array.isArray(value.tasks) ||
|
|
434
|
+
!Number.isFinite(Date.parse(value.created)) || !Number.isFinite(Date.parse(value.updated))) fail('invalid stamp record: ' + file);
|
|
435
|
+
for (const pack of value.packs) {
|
|
436
|
+
if (!['nuget','npm'].includes(pack.ecosystem) || !Array.isArray(pack.ids) || pack.ids.some(id => typeof id !== 'string') ||
|
|
437
|
+
!pack.producer || ['repo','worktree','branch','head'].some(k => typeof pack.producer[k] !== 'string') ||
|
|
438
|
+
!(pack.task === null || typeof pack.task === 'string' && taskPattern.test(pack.task)) || !Number.isFinite(Date.parse(pack.time))) fail('invalid pack record: ' + file);
|
|
439
|
+
}
|
|
440
|
+
for (const consumer of value.consumers) {
|
|
441
|
+
if (['worktree','session','ecosystem','time'].some(k => typeof consumer[k] !== 'string') || !['nuget','npm'].includes(consumer.ecosystem) ||
|
|
442
|
+
!(consumer.task === null || typeof consumer.task === 'string' && taskPattern.test(consumer.task)) ||
|
|
443
|
+
!Number.isFinite(Date.parse(consumer.time)) || consumer.restored && !Number.isFinite(Date.parse(consumer.restored))) fail('invalid consumer record: ' + file);
|
|
444
|
+
}
|
|
445
|
+
for (const [id, state] of Object.entries(value.tasks)) {
|
|
446
|
+
if (!taskPattern.test(id) || !state || !(state.finished === null || typeof state.finished === 'string' && Number.isFinite(Date.parse(state.finished)))) fail('invalid task record: ' + file);
|
|
447
|
+
}
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
function warning(stamp, error) {
|
|
451
|
+
console.error('WARNING: stamp registry: ' + path.join(root, stamp, 'record.json') + ': ' + error.message);
|
|
452
|
+
process.exitCode = 1;
|
|
453
|
+
}
|
|
454
|
+
function sessionStamps(session) {
|
|
455
|
+
const files = ['stamp','Directory.Packages.props','cratis-local.targets','nuget.config','env','npm-backup/expected-package.json','composition/manifest.tsv'];
|
|
456
|
+
const found = new Set();
|
|
457
|
+
for (const name of files) {
|
|
458
|
+
const file = path.join(session, name);
|
|
459
|
+
if (!fs.existsSync(file)) continue;
|
|
460
|
+
for (const stamp of fs.readFileSync(file, 'utf8').match(/[0-9]+\.[0-9]+\.[0-9]+-(?:local\.[0-9]{12}|train)\.g[0-9a-f]{7}/g) || []) found.add(stamp);
|
|
461
|
+
}
|
|
462
|
+
for (const stamp of found) console.log(stamp);
|
|
463
|
+
}
|
|
464
|
+
function stamps() {
|
|
465
|
+
safe(root, true);
|
|
466
|
+
if (!fs.existsSync(root)) return [];
|
|
467
|
+
return fs.readdirSync(root).filter(s => stampPattern.test(s)).sort();
|
|
468
|
+
}
|
|
469
|
+
function write(value) {
|
|
470
|
+
const file = recordPath(value.stamp), next = file + '.next'; safe(next);
|
|
471
|
+
fs.mkdirSync(path.dirname(file), {recursive:true, mode:0o700});
|
|
472
|
+
value.updated = now;
|
|
473
|
+
fs.writeFileSync(next, JSON.stringify(value, null, 2) + '\n', {mode:0o600});
|
|
474
|
+
fs.chmodSync(next, 0o600); fs.renameSync(next, file);
|
|
475
|
+
}
|
|
476
|
+
function consumerState(consumer) {
|
|
477
|
+
if (consumer.restored) return 'restored';
|
|
478
|
+
if (!fs.existsSync(consumer.worktree)) return 'gone';
|
|
479
|
+
const git = cp.spawnSync('git', ['-C', consumer.worktree, 'rev-parse', '--show-toplevel'], {encoding:'utf8'});
|
|
480
|
+
if (git.status !== 0 || git.stdout.trim() !== consumer.worktree) return 'gone';
|
|
481
|
+
return ['env','Directory.Packages.props','cratis-local.targets','nuget.config','npm-backup','composition'].some(n => fs.existsSync(path.join(consumer.session,n))) ? 'live' : 'restored';
|
|
482
|
+
}
|
|
483
|
+
function packages() {
|
|
484
|
+
const found = new Map();
|
|
485
|
+
for (const ecosystem of ['nuget','npm']) {
|
|
486
|
+
const directory = path.join(feed,ecosystem); safe(directory,true);
|
|
487
|
+
if (!fs.existsSync(directory)) continue;
|
|
488
|
+
for (const name of fs.readdirSync(directory)) {
|
|
489
|
+
const match = name.match(/^(.*?)[.-]([0-9]+\.[0-9]+\.[0-9]+-(?:local\.[0-9]{12}|train)\.g[0-9a-f]{7})\.(nupkg|tgz)$/);
|
|
490
|
+
if (!match || (ecosystem === 'nuget' ? match[3] !== 'nupkg' : match[3] !== 'tgz')) continue;
|
|
491
|
+
const file = path.join(directory,name); safe(file);
|
|
492
|
+
let id = match[1];
|
|
493
|
+
if (ecosystem === 'npm') id = JSON.parse(cp.execFileSync('tar',['-xOf',file,'package/package.json'],{encoding:'utf8'})).name;
|
|
494
|
+
if (typeof id !== 'string' || !id) fail('invalid package ID: ' + file);
|
|
495
|
+
const entries = found.get(match[2]) || [];
|
|
496
|
+
entries.push({id,version:match[2],ecosystem,file}); found.set(match[2],entries);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return found;
|
|
500
|
+
}
|
|
501
|
+
function worktreeGone(consumer) {
|
|
502
|
+
if (!fs.statSync(consumer.worktree,{throwIfNoEntry:false})?.isDirectory()) return true;
|
|
503
|
+
const git = cp.spawnSync('git',['-C',consumer.worktree,'rev-parse','--show-toplevel'],{encoding:'utf8'});
|
|
504
|
+
return git.status !== 0 || git.stdout.trim() !== consumer.worktree;
|
|
505
|
+
}
|
|
506
|
+
function gone(consumer) {
|
|
507
|
+
return !fs.statSync(consumer.session,{throwIfNoEntry:false})?.isDirectory() || worktreeGone(consumer);
|
|
508
|
+
}
|
|
509
|
+
function expiryInputs() {
|
|
510
|
+
const selectedIndex = args.indexOf('--stamp'), selected = selectedIndex < 0 ? null : args[selectedIndex + 1];
|
|
511
|
+
const sessions = path.join(feed,'sessions'); safe(sessions);
|
|
512
|
+
if (fs.existsSync(sessions)) {
|
|
513
|
+
for (const session of fs.readFileSync(sessions,'utf8').split('\n').filter(Boolean)) {
|
|
514
|
+
const repo = path.join(session,'repo');
|
|
515
|
+
if (fs.existsSync(repo)) console.log(fs.readFileSync(repo,'utf8').trim() + '\t' + session);
|
|
516
|
+
else console.log('\t' + session);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const stamp of selected ? [selected] : stamps()) {
|
|
520
|
+
let value;
|
|
521
|
+
try { value = read(stamp); } catch (error) { if (args[0] === 'list') continue; throw error; }
|
|
522
|
+
for (const c of value?.consumers || []) console.log(c.worktree + '\t' + c.session);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function contextLines(name) { return fs.readFileSync(path.join(scratch,'expiry-' + name),'utf8').split('\n').filter(Boolean); }
|
|
526
|
+
function atLeast(version, base) {
|
|
527
|
+
if (!/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(version)) return false;
|
|
528
|
+
const a = version.split('.').map(BigInt), b = base.split('.').map(BigInt);
|
|
529
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] > b[i];
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
function offlineRelease(value) {
|
|
533
|
+
for (const pack of value.packs.filter(p => p.ecosystem === 'nuget')) for (const id of pack.ids) {
|
|
534
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(id)) fail('invalid NuGet release ID: ' + id);
|
|
535
|
+
for (const cache of contextLines('caches')) {
|
|
536
|
+
const directory = path.join(cache,id.toLowerCase());
|
|
537
|
+
// A missing cache is no evidence; actual scan/read errors fail closed.
|
|
538
|
+
if (!fs.statSync(directory,{throwIfNoEntry:false})) continue;
|
|
539
|
+
for (const version of fs.readdirSync(directory).sort()) {
|
|
540
|
+
if (atLeast(version,value.base) && fs.statSync(path.join(directory,version)).isDirectory() &&
|
|
541
|
+
fs.statSync(path.join(directory,version,'.nupkg.metadata'),{throwIfNoEntry:false})?.isFile()) return 'released:' + id + '@' + version;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
async function onlineRelease(value) {
|
|
548
|
+
for (const pack of value.packs) for (const id of pack.ids) {
|
|
549
|
+
// Test-only override: hermetic unreachable-host fixtures, never a feed setting.
|
|
550
|
+
const override = process.env.CRATIS_LOCAL_RELEASE_URL_BASE;
|
|
551
|
+
const url = pack.ecosystem === 'nuget' ? (override || 'https://api.nuget.org/v3-flatcontainer') + '/' + encodeURIComponent(id.toLowerCase()) + '/index.json' :
|
|
552
|
+
(override || 'https://registry.npmjs.org') + '/' + encodeURIComponent(id);
|
|
553
|
+
try {
|
|
554
|
+
const response = await fetch(url,{signal:AbortSignal.timeout(10000)});
|
|
555
|
+
if (!response.ok) continue;
|
|
556
|
+
const data = await response.json(), versions = pack.ecosystem === 'nuget' ? data.versions : Object.keys(data.versions || {});
|
|
557
|
+
if (!Array.isArray(versions)) continue;
|
|
558
|
+
const version = versions.find(v => typeof v === 'string' && atLeast(v,value.base));
|
|
559
|
+
if (version) return 'released:' + id + '@' + version;
|
|
560
|
+
} catch { /* Network failures are not release evidence and never fail cleanup. */ }
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
async function expiry(stamp, value, online = false) {
|
|
565
|
+
const kept = reason => ({expired:false,reasons:[],kept:[reason]});
|
|
566
|
+
if (!value) return kept('unrecorded; use clean --stamp ' + stamp);
|
|
567
|
+
if (contextLines('compositions').includes(stamp)) return kept('active composition member');
|
|
568
|
+
const reference = contextLines('live').find(line => line.split('\t')[0] === stamp);
|
|
569
|
+
if (reference) return kept('live reference in ' + reference.slice(reference.indexOf('\t') + 1));
|
|
570
|
+
const tasks = value.packs.map(p => p.task).filter(t => t !== null);
|
|
571
|
+
let reason = tasks.length && tasks.every(t => value.tasks[t]?.finished) ? 'task-finished' : null;
|
|
572
|
+
if (!reason && value.consumers.length && value.consumers.every(gone)) reason = 'consumers-gone';
|
|
573
|
+
if (!reason) reason = offlineRelease(value);
|
|
574
|
+
if (!reason && online) reason = await onlineRelease(value);
|
|
575
|
+
return reason ? {expired:true,reasons:[reason],kept:[]} : kept('active');
|
|
576
|
+
}
|
|
577
|
+
async function expired() {
|
|
578
|
+
const names = [...new Set([...packages().keys(),...stamps()])].sort();
|
|
579
|
+
// Validate every subject before reporting decisions or allowing any deletion.
|
|
580
|
+
const records = names.map(stamp => [stamp,read(stamp)]);
|
|
581
|
+
for (const [stamp,value] of records) {
|
|
582
|
+
const decision = await expiry(stamp,value,args.includes('--online'));
|
|
583
|
+
console.log([decision.expired ? 'expired' : 'kept',stamp,(decision.expired ? decision.reasons : decision.kept).join('; ')].join('\t'));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function prune() {
|
|
587
|
+
const values = stamps().map(read), sessions = path.join(feed,'sessions'); safe(sessions);
|
|
588
|
+
const before = fs.existsSync(sessions) ? fs.readFileSync(sessions,'utf8') : null;
|
|
589
|
+
for (const value of values.filter(Boolean)) {
|
|
590
|
+
// Keep a consumer while its worktree exists: its manifests are still scanned for live references.
|
|
591
|
+
const consumers = value.consumers.filter(c => !worktreeGone(c));
|
|
592
|
+
if (consumers.length !== value.consumers.length) { value.consumers = consumers; write(value); }
|
|
593
|
+
}
|
|
594
|
+
if (before !== null) {
|
|
595
|
+
const after = before.split('\n').filter(s => s && fs.statSync(s,{throwIfNoEntry:false})?.isDirectory()).join('\n');
|
|
596
|
+
const next = sessions + '.next'; safe(next);
|
|
597
|
+
fs.writeFileSync(next,after ? after + '\n' : '',{mode:0o600}); fs.renameSync(next,sessions);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function list() {
|
|
601
|
+
const json = args.includes('--json'), index = args.indexOf('--stamp');
|
|
602
|
+
const selected = index < 0 ? null : args[index + 1];
|
|
603
|
+
const remaining = args.filter((_,i) => i !== index && i !== index + 1 || index < 0);
|
|
604
|
+
if (remaining.some(a => a !== '--json') || index >= 0 && !stampPattern.test(selected || '')) fail('list accepts --json and --stamp STAMP');
|
|
605
|
+
const artifacts = packages(), names = [...new Set([...artifacts.keys(), ...stamps()])].sort();
|
|
606
|
+
const rows = [];
|
|
607
|
+
for (const stamp of names.filter(s => !selected || s === selected)) {
|
|
608
|
+
let value = null, error;
|
|
609
|
+
try { value = read(stamp); } catch (failure) { error = failure.message; warning(stamp, failure); }
|
|
610
|
+
const match = stamp.match(stampPattern), created = value?.created || null;
|
|
611
|
+
rows.push({stamp,base:match[1],kind:match[2] === 'train' ? 'train' : 'local',recorded:error ? 'invalid' : !!value,...(error ? {error} : {}),packages:artifacts.get(stamp) || [],
|
|
612
|
+
producers:value?.packs.map(p => p.producer) || [],tasks:Object.entries(value?.tasks || {}).map(([id,s]) => ({id,finished:s.finished})),
|
|
613
|
+
consumers:value?.consumers.map(c => ({worktree:c.worktree,task:c.task,state:consumerState(c)})) || [],created,
|
|
614
|
+
ageSeconds:created ? Math.max(0,Math.floor((Date.now()-Date.parse(created))/1000)) : null,
|
|
615
|
+
expiry:error ? {expired:false,reasons:[],kept:['invalid record']} : await expiry(stamp,value)});
|
|
616
|
+
}
|
|
617
|
+
if (json) console.log(JSON.stringify({schema:1,feed,stamps:rows}));
|
|
618
|
+
else {
|
|
619
|
+
console.log('STAMP\tECOSYS\tPACKAGES(n)\tOWNER(repo@branch)\tTASK(state)\tCONSUMERS(live/total)\tAGE\tRECORDED');
|
|
620
|
+
for (const r of rows) console.log([r.stamp,[...new Set(r.packages.map(p => p.ecosystem))].join(',') || '-',r.packages.length,
|
|
621
|
+
r.producers.map(p => p.repo+'@'+p.branch).join(',') || '-',r.tasks.map(t => t.id+'('+ (t.finished ? 'finished' : 'active') +')').join(',') || '-',
|
|
622
|
+
r.consumers.filter(c => c.state === 'live').length+'/'+r.consumers.length,r.ageSeconds === null ? '-' : r.ageSeconds+'s',r.recorded].join('\t'));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function update() {
|
|
626
|
+
const [stamp, ecosystem, repo, session, cache, ...ids] = args;
|
|
627
|
+
let value = read(stamp);
|
|
628
|
+
if (!value) {
|
|
629
|
+
const match = stamp.match(stampPattern);
|
|
630
|
+
value = {schema:1,stamp,base:match[1],kind:match[2] === 'train' ? 'train' : 'local',created:now,updated:now,packs:[],consumers:[],tasks:{}};
|
|
631
|
+
}
|
|
632
|
+
if (task && !Object.hasOwn(value.tasks,task)) Object.defineProperty(value.tasks,task,{value:{finished:null},enumerable:true,writable:true,configurable:true});
|
|
633
|
+
if (operation === 'pack') {
|
|
634
|
+
const git = (...argv) => cp.execFileSync('git',['-C',repo,...argv],{encoding:'utf8'}).trim();
|
|
635
|
+
const producer = {repo:path.basename(repo),worktree:repo,branch:git('branch','--show-current'),head:git('rev-parse','HEAD')};
|
|
636
|
+
const entry = {ecosystem,ids,producer,task,time:now};
|
|
637
|
+
const index = value.packs.findIndex(p => p.ecosystem === ecosystem && p.producer.worktree === repo && p.task === task);
|
|
638
|
+
if (index < 0) value.packs.push(entry); else value.packs[index] = {...entry,ids:[...new Set([...value.packs[index].ids,...ids])]};
|
|
639
|
+
} else {
|
|
640
|
+
const entry = {worktree:repo,session,task,ecosystem,time:now,...(cache ? {cache} : {})};
|
|
641
|
+
const index = value.consumers.findIndex(c => c.worktree === repo && c.session === session && c.ecosystem === ecosystem && c.task === task);
|
|
642
|
+
if (index < 0) value.consumers.push(entry); else value.consumers[index] = entry;
|
|
643
|
+
}
|
|
644
|
+
write(value);
|
|
645
|
+
}
|
|
646
|
+
async function main() {
|
|
647
|
+
if (operation === 'list') await list();
|
|
648
|
+
else if (operation === 'expiry-inputs') expiryInputs();
|
|
649
|
+
else if (operation === 'expiry') await expired();
|
|
650
|
+
else if (operation === 'prune') prune();
|
|
651
|
+
else if (operation === 'pack' || operation === 'use') update();
|
|
652
|
+
else if (operation === 'restore-stamps') sessionStamps(args[0]);
|
|
653
|
+
else if (operation === 'remove') {
|
|
654
|
+
const file = recordPath(args[0]); read(args[0]);
|
|
655
|
+
if (fs.readdirSync(path.dirname(file)).some(n => n !== 'record.json')) fail('unexpected stamp directory entry');
|
|
656
|
+
fs.unlinkSync(file); fs.rmdirSync(path.dirname(file));
|
|
657
|
+
} else {
|
|
658
|
+
const selected = operation === 'restore' ? [...new Set(args.slice(1))] :
|
|
659
|
+
operation === 'clean' && args[0] && args[0] !== 'all' ? [args[0]] : stamps();
|
|
660
|
+
let count = 0;
|
|
661
|
+
for (const stamp of selected) {
|
|
662
|
+
let value;
|
|
663
|
+
try { value = read(stamp); } catch (error) {
|
|
664
|
+
if (operation !== 'finish' && operation !== 'restore') throw error;
|
|
665
|
+
warning(stamp, error); continue;
|
|
666
|
+
}
|
|
667
|
+
if (!value) continue;
|
|
668
|
+
if (operation === 'finish' && Object.hasOwn(value.tasks,task)) {
|
|
669
|
+
console.log((args.includes('--apply') ? 'finish: ' : 'would finish: ') + stamp + ' ' + task); count++;
|
|
670
|
+
if (args.includes('--apply')) { value.tasks[task].finished = now; write(value); }
|
|
671
|
+
} else if (operation === 'restore' && value.consumers.some(c => c.session === args[0] && !c.restored)) {
|
|
672
|
+
for (const c of value.consumers) if (c.session === args[0] && !c.restored) c.restored = now;
|
|
673
|
+
write(value);
|
|
674
|
+
} else if (operation === 'clean' && (!args[0] || args[0] === 'all' || stamp === args[0])) {
|
|
675
|
+
const directory = path.dirname(recordPath(stamp));
|
|
676
|
+
if (fs.readdirSync(directory).some(n => n !== 'record.json')) fail('unexpected stamp directory entry: ' + directory);
|
|
677
|
+
console.log(directory);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (operation === 'finish' && !count) console.log('no records name task ' + task);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
main().catch(error => { console.error('ERROR: stamp registry: ' + error.message); process.exitCode = 2; });
|
|
684
|
+
REGISTRY
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
# Compare file identities/times so a multi-producer stamp does not attribute earlier
|
|
688
|
+
# producers' packages to the current pack. Repacking the same ID is still recorded.
|
|
689
|
+
nuget_pack_files() {
|
|
690
|
+
node - "$FEED/nuget" "$@" <<'NODE'
|
|
691
|
+
const fs = require('node:fs'), path = require('node:path');
|
|
692
|
+
const [directory, stamp, before] = process.argv.slice(2);
|
|
693
|
+
const files = {};
|
|
694
|
+
for (const name of fs.existsSync(directory) ? fs.readdirSync(directory) : []) {
|
|
695
|
+
if (!name.endsWith('.' + stamp + '.nupkg')) continue;
|
|
696
|
+
const file = path.join(directory,name), st = fs.statSync(file, {bigint:true});
|
|
697
|
+
files[file] = [st.ino,st.size,st.mtimeNs,st.ctimeNs].map(String).join(':');
|
|
698
|
+
}
|
|
699
|
+
if (!before) console.log(JSON.stringify(files));
|
|
700
|
+
else {
|
|
701
|
+
const previous = JSON.parse(fs.readFileSync(before,'utf8'));
|
|
702
|
+
for (const [file,identity] of Object.entries(files)) if (previous[file] !== identity) process.stdout.write(file + '\0');
|
|
703
|
+
}
|
|
704
|
+
NODE
|
|
705
|
+
}
|
|
706
|
+
|
|
316
707
|
pack_nuget() {
|
|
317
708
|
[ $# -ge 3 ] || die 'pack-nuget requires producer, stamp and targets'
|
|
318
|
-
local repo version target path count=0 property
|
|
709
|
+
local repo version target path count=0 property ids=()
|
|
319
710
|
local properties=()
|
|
320
711
|
repo=$(repo_root "$1"); version=$2; shift 2
|
|
321
712
|
valid_stamp "$version"; need dotnet
|
|
@@ -328,16 +719,23 @@ pack_nuget() {
|
|
|
328
719
|
properties+=("-p:$property")
|
|
329
720
|
done
|
|
330
721
|
[ $# -ge 1 ] || die 'pack-nuget requires at least one target after the properties'
|
|
722
|
+
if [ "$DRY" -eq 0 ]; then
|
|
723
|
+
need node; scratch
|
|
724
|
+
nuget_pack_files "$version" > "$TMP/pack-before"
|
|
725
|
+
fi
|
|
331
726
|
mutate mkdir -p "$FEED/nuget"
|
|
332
727
|
for target in "$@"; do
|
|
333
728
|
(cd "$repo" && heavy build "pack NuGet $target" dotnet pack "$target" -c Release "-p:PackageVersion=$version" "-p:Version=$version" ${properties[@]+"${properties[@]}"} -o "$FEED/nuget")
|
|
334
729
|
done
|
|
335
730
|
[ "$DRY" -eq 0 ] || return 0
|
|
731
|
+
nuget_pack_files "$version" "$TMP/pack-before" > "$TMP/pack-produced"
|
|
336
732
|
while IFS= read -r -d '' path; do
|
|
337
733
|
printf '%s\n' "$path"; count=$((count + 1))
|
|
338
|
-
|
|
734
|
+
target=${path##*/}; ids+=("${target%."$version".nupkg}")
|
|
735
|
+
done < "$TMP/pack-produced"
|
|
339
736
|
[ "$count" -gt 0 ] || failed 'pack produced no matching nupkgs'
|
|
340
737
|
printf 'produced %s nupkgs\n' "$count"
|
|
738
|
+
registry_update pack "$version" nuget "$repo" '' '' "${ids[@]}"
|
|
341
739
|
}
|
|
342
740
|
|
|
343
741
|
# Pack one workspace with npm pack (what publish does) into the feed as <scope>-<name>-<version>.tgz.
|
|
@@ -358,7 +756,7 @@ pack_npm_workspace() {
|
|
|
358
756
|
|
|
359
757
|
pack_npm() {
|
|
360
758
|
[ $# -eq 2 ] || die 'pack-npm requires producer and stamp'
|
|
361
|
-
local repo version path count=0
|
|
759
|
+
local repo version path count=0 ids=()
|
|
362
760
|
repo=$(repo_root "$1"); version=$2
|
|
363
761
|
valid_stamp "$version"; need node; need npm; need yarn; scratch
|
|
364
762
|
git -C "$repo" diff --name-only HEAD -- 'package.json' '*/package.json' > "$TMP/modified"
|
|
@@ -409,6 +807,10 @@ NODE
|
|
|
409
807
|
done < <(find "$FEED/npm" -type f -name "*-$version.tgz" -print0)
|
|
410
808
|
[ "$count" -gt 0 ] || failed 'pack produced no matching npm tarballs'
|
|
411
809
|
printf 'produced %s npm tarballs\n' "$count"
|
|
810
|
+
while IFS= read -r -d '' path; do
|
|
811
|
+
ids+=("$(node -p 'require(process.argv[1]).name' "$repo/$path")")
|
|
812
|
+
done < "$TMP/manifests"
|
|
813
|
+
registry_update pack "$version" npm "$repo" '' '' "${ids[@]}"
|
|
412
814
|
}
|
|
413
815
|
|
|
414
816
|
source_mapping() {
|
|
@@ -478,10 +880,35 @@ nuget_env() {
|
|
|
478
880
|
printf 'if [ "%s" != %q ]; then\n' "\${$targets_key:-}" "$session/cratis-local.targets"
|
|
479
881
|
printf ' export _CratisLocalPreviousAfterMicrosoftCommonTargets="%s"\nfi\n' "\${$targets_key:-}"
|
|
480
882
|
printf 'export %s=%q\n' "$targets_key" "$session/cratis-local.targets"
|
|
883
|
+
if [ -n "${PRIVATE_NUGET_CACHE:-}" ]; then printf 'export NUGET_PACKAGES=%q\n' "$PRIVATE_NUGET_CACHE"; fi
|
|
481
884
|
}
|
|
482
885
|
|
|
483
886
|
use_nuget() {
|
|
484
|
-
|
|
887
|
+
local private=${CRATIS_LOCAL_PRIVATE_CACHE:-0} PRIVATE_NUGET_CACHE='' option filtered=() private_repo private_session
|
|
888
|
+
for option in "$@"; do
|
|
889
|
+
if [ "$option" = --private-cache ]; then private=1; else filtered+=("$option"); fi
|
|
890
|
+
done
|
|
891
|
+
set -- ${filtered[@]+"${filtered[@]}"}
|
|
892
|
+
if [ "$private" = 1 ]; then
|
|
893
|
+
[ $# -ge 2 ] || die 'use-nuget requires consumer and stamp or composition'
|
|
894
|
+
private_repo=$(repo_root "$1"); private_session=$(session_path "$private_repo")
|
|
895
|
+
PRIVATE_NUGET_CACHE=$(git -C "$private_repo" rev-parse --absolute-git-dir)/cratis-local-nuget
|
|
896
|
+
prepare_private_cache "$private_repo" "$PRIVATE_NUGET_CACHE" "$private_session"
|
|
897
|
+
fi
|
|
898
|
+
if [ "${2:-}" = --composition ]; then
|
|
899
|
+
composition use-nuget "$@"
|
|
900
|
+
if [ "$private" = 1 ] && [ "$DRY" -eq 0 ]; then register_private_cache "$private_repo" "$PRIVATE_NUGET_CACHE" "$private_session"; fi
|
|
901
|
+
if [ "$DRY" -eq 0 ]; then
|
|
902
|
+
local composition_repo composition_session composition_stamp
|
|
903
|
+
composition_repo=$(repo_root "$1"); composition_session=$(session_path "$composition_repo")
|
|
904
|
+
scratch
|
|
905
|
+
cut -f3 "$composition_session/composition/manifest.tsv" | sort -u > "$TMP/registry-stamps"
|
|
906
|
+
while IFS= read -r composition_stamp; do
|
|
907
|
+
registry_consumer "$composition_stamp" nuget "$composition_repo" "$composition_session" "$PRIVATE_NUGET_CACHE"
|
|
908
|
+
done < "$TMP/registry-stamps"
|
|
909
|
+
fi
|
|
910
|
+
return
|
|
911
|
+
fi
|
|
485
912
|
case " $* " in *' --composition '*) die 'cannot mix a stamp or IDs with --composition' ;; esac
|
|
486
913
|
[ $# -ge 2 ] || die 'use-nuget requires consumer and stamp'
|
|
487
914
|
[ ! -d "$(session_path "$(repo_root "$1")")/composition" ] || die 'active composition; restore first'
|
|
@@ -527,6 +954,7 @@ use_nuget() {
|
|
|
527
954
|
return 0
|
|
528
955
|
fi
|
|
529
956
|
register_session "$repo" "$session" "$version"
|
|
957
|
+
if [ "$private" = 1 ]; then register_private_cache "$repo" "$PRIVATE_NUGET_CACHE" "$session"; fi
|
|
530
958
|
{
|
|
531
959
|
nuget_props_start "$pin"
|
|
532
960
|
for id in "${ids[@]}"; do
|
|
@@ -558,8 +986,11 @@ use_nuget() {
|
|
|
558
986
|
elif [ -f "$session/nuget.config" ]; then
|
|
559
987
|
rm -- "$session/nuget.config"
|
|
560
988
|
fi
|
|
989
|
+
registry_consumer "$version" nuget "$repo" "$session" "$PRIVATE_NUGET_CACHE"
|
|
561
990
|
printf 'overrode %s NuGet package IDs\n' "${#ids[@]}"
|
|
562
|
-
|
|
991
|
+
if [ "$private" != 1 ]; then
|
|
992
|
+
printf 'Optional per-worktree cache (set before restore and clean): export NUGET_PACKAGES=%q\n' "$(git -C "$repo" rev-parse --absolute-git-dir)/cratis-local-nuget"
|
|
993
|
+
fi
|
|
563
994
|
}
|
|
564
995
|
|
|
565
996
|
# A shared lock covers legacy mutations too: registration rollback must never replace
|
|
@@ -952,7 +1383,7 @@ function activate(packages,pin) {
|
|
|
952
1383
|
const props = path.join(repo,'Directory.Packages.props'); plain(props);
|
|
953
1384
|
if (exists(session)) {
|
|
954
1385
|
plain(session,true);
|
|
955
|
-
const allowed = new Set(['repo','composition']);
|
|
1386
|
+
const allowed = new Set(['repo','composition','nuget-cache']);
|
|
956
1387
|
if (fs.readdirSync(session).some(n => !allowed.has(n))) fail('conflicting active overlay/session; restore first');
|
|
957
1388
|
if (exists(path.join(session,'repo')) && fs.readFileSync(path.join(session,'repo'),'utf8') !== repo + '\n') fail('session belongs to another checkout');
|
|
958
1389
|
}
|
|
@@ -1083,14 +1514,15 @@ function cleanGuard() {
|
|
|
1083
1514
|
if (!exists(composition)) continue;
|
|
1084
1515
|
plain(composition,true); plain(path.join(composition,'manifest.tsv'));
|
|
1085
1516
|
const versions = fs.readFileSync(path.join(composition,'manifest.tsv'),'utf8').trim().split('\n').map(row => row.split('\t')[2]);
|
|
1086
|
-
if (
|
|
1517
|
+
if (mode === 'clean-members') { for (const version of versions) console.log(version); }
|
|
1518
|
+
else if (!selected || selected === 'all' || versions.includes(selected)) fail('active composition retains member artifacts; restore first: ' + known);
|
|
1087
1519
|
}
|
|
1088
1520
|
}
|
|
1089
1521
|
try {
|
|
1090
1522
|
safeParents(feed);
|
|
1091
1523
|
if (mode === 'recover') recover();
|
|
1092
1524
|
else if (mode === 'restore-state') restoreState();
|
|
1093
|
-
else if (mode === 'clean-guard') cleanGuard();
|
|
1525
|
+
else if (mode === 'clean-guard' || mode === 'clean-members') cleanGuard();
|
|
1094
1526
|
else {
|
|
1095
1527
|
const args = [...argv]; if (mode !== 'verify-pack') args.shift();
|
|
1096
1528
|
if (args.shift() !== '--composition' || !args.length) fail('expected --composition FILE');
|
|
@@ -1629,17 +2061,30 @@ NODE
|
|
|
1629
2061
|
git -C "$dir" diff HEAD -- package.json yarn.lock > "$backup/before.diff"
|
|
1630
2062
|
cp "$TMP/package.json" "$backup/expected-package.json"
|
|
1631
2063
|
cp "$TMP/package.json" "$dir/package.json"
|
|
2064
|
+
registry_consumer "$version" npm "$repo" "$session" ''
|
|
1632
2065
|
printf 'Next: cd %q && yarn install\n' "$dir"
|
|
1633
2066
|
printf 'Vite: clear stale prebundles with rm -rf node_modules/.vite\n'
|
|
1634
2067
|
}
|
|
1635
2068
|
|
|
1636
2069
|
restore() {
|
|
1637
2070
|
[ $# -eq 1 ] || die 'restore requires a consumer'
|
|
1638
|
-
local repo session backup dir
|
|
2071
|
+
local repo session backup dir stamp
|
|
2072
|
+
local restore_stamps=()
|
|
1639
2073
|
repo=$(repo_root "$1"); session=$(session_path "$repo"); backup=$session/npm-backup
|
|
1640
2074
|
validate_session "$session"
|
|
2075
|
+
scratch
|
|
2076
|
+
# Restoring the overlay never depends on node; only the registry bookkeeping does.
|
|
2077
|
+
local registry_node=0
|
|
2078
|
+
if [ "$DRY" -eq 0 ] && command -v node > /dev/null 2>&1; then
|
|
2079
|
+
registry_node=1
|
|
2080
|
+
registry restore-stamps "$session" > "$TMP/restore-stamps"
|
|
2081
|
+
while IFS= read -r stamp; do restore_stamps+=("$stamp"); done < "$TMP/restore-stamps"
|
|
2082
|
+
elif [ "$DRY" -eq 0 ]; then
|
|
2083
|
+
printf 'WARNING: node not found; restoring without updating the stamp registry\n' >&2
|
|
2084
|
+
fi
|
|
1641
2085
|
if [ -d "$session/composition" ]; then
|
|
1642
2086
|
composition restore-state "$repo"
|
|
2087
|
+
if [ "$registry_node" -eq 1 ]; then registry_update restore "$session" ${restore_stamps[@]+"${restore_stamps[@]}"}; fi
|
|
1643
2088
|
if [ "$DRY" -eq 0 ]; then check "$repo"; fi
|
|
1644
2089
|
return
|
|
1645
2090
|
fi
|
|
@@ -1659,6 +2104,7 @@ restore() {
|
|
|
1659
2104
|
mutate rm -rf -- "$backup"
|
|
1660
2105
|
fi
|
|
1661
2106
|
mutate rm -f -- "$session/Directory.Packages.props" "$session/cratis-local.targets" "$session/nuget.config" "$session/env" "$session/stamp" "$session/npm-dir"
|
|
2107
|
+
if [ "$registry_node" -eq 1 ]; then registry_update restore "$session" ${restore_stamps[@]+"${restore_stamps[@]}"}; fi
|
|
1662
2108
|
if [ "$DRY" -eq 0 ]; then check "$repo"; fi
|
|
1663
2109
|
}
|
|
1664
2110
|
|
|
@@ -1738,20 +2184,11 @@ bounded_docker() {
|
|
|
1738
2184
|
local_artifact() {
|
|
1739
2185
|
[[ "$1" =~ -(local\.[0-9]{12}|train)\.g[0-9a-f]{7}($|[^0-9A-Za-z]) ]]
|
|
1740
2186
|
}
|
|
1741
|
-
#
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
while IFS= read -r session; do
|
|
1747
|
-
[ -d "$session" ] && [ -f "$session/repo" ] || continue
|
|
1748
|
-
validate_session "$session"
|
|
1749
|
-
repo=$(cat "$session/repo")
|
|
1750
|
-
[ -d "$repo" ] || continue
|
|
1751
|
-
[ "$(repo_root "$repo")" = "$repo" ] || die "registered consumer no longer matches repository: $repo"
|
|
1752
|
-
session_selected=0
|
|
1753
|
-
if [ -f "$session/stamp" ] && [ "$(cat "$session/stamp")" = "$selected" ]; then session_selected=1; fi
|
|
1754
|
-
# Follow manifest symlinks too; traversal/read failures refuse cleanup.
|
|
2187
|
+
# Print stamp<TAB>file. Follow manifest symlinks; traversal/read errors refuse cleanup.
|
|
2188
|
+
live_stamps() {
|
|
2189
|
+
local repo session file found stamp status
|
|
2190
|
+
for repo in "$@"; do
|
|
2191
|
+
session=${LIVE_SESSION:-$(session_path "$repo")}
|
|
1755
2192
|
find -L "$repo" \( -name .git -o -name node_modules \) -prune -o \( -iname nuget.config -o -iname Directory.Packages.props -o -name packages.lock.json -o -name package.json -o -name yarn.lock \) \( -type f -o -type l \) -print0 > "$TMP/live-files" || die "cannot inspect registered consumer: $repo"
|
|
1756
2193
|
for file in "$session/Directory.Packages.props" "$session/cratis-local.targets" "$session/nuget.config"; do
|
|
1757
2194
|
if [ -e "$file" ]; then printf '%s\0' "$file" >> "$TMP/live-files"; fi
|
|
@@ -1764,35 +2201,96 @@ clean_live_references() {
|
|
|
1764
2201
|
[ -n "$found" ] || continue
|
|
1765
2202
|
while IFS= read -r stamp; do
|
|
1766
2203
|
stamp=$(printf '%s' "$stamp" | tr '[:upper:]' '[:lower:]')
|
|
1767
|
-
|
|
1768
|
-
printf 'kept stamp %s: registered live reference in %s\n' "$stamp" "$file"
|
|
1769
|
-
blocked=1
|
|
1770
|
-
fi
|
|
2204
|
+
printf '%s\t%s\n' "$stamp" "$file"
|
|
1771
2205
|
done <<< "$found"
|
|
1772
2206
|
done < "$TMP/live-files"
|
|
2207
|
+
done
|
|
2208
|
+
}
|
|
2209
|
+
# Legacy clean still refuses the whole request, with its original messages.
|
|
2210
|
+
clean_live_references() {
|
|
2211
|
+
local selected=$1 session repo file stamp blocked=0 session_selected
|
|
2212
|
+
while IFS= read -r session; do
|
|
2213
|
+
[ -d "$session" ] && [ -f "$session/repo" ] || continue
|
|
2214
|
+
validate_session "$session"
|
|
2215
|
+
repo=$(cat "$session/repo")
|
|
2216
|
+
[ -d "$repo" ] || continue
|
|
2217
|
+
[ "$(repo_root "$repo")" = "$repo" ] || die "registered consumer no longer matches repository: $repo"
|
|
2218
|
+
session_selected=0
|
|
2219
|
+
if [ -f "$session/stamp" ] && [ "$(cat "$session/stamp")" = "$selected" ]; then session_selected=1; fi
|
|
2220
|
+
LIVE_SESSION=$session live_stamps "$repo" > "$TMP/legacy-live"
|
|
2221
|
+
while IFS=$'\t' read -r stamp file; do
|
|
2222
|
+
if [ -z "$selected" ] || [ "$selected" = all ] || [ "$session_selected" -eq 1 ] || [ "$stamp" = "$selected" ]; then
|
|
2223
|
+
printf 'kept stamp %s: registered live reference in %s\n' "$stamp" "$file"
|
|
2224
|
+
blocked=1
|
|
2225
|
+
fi
|
|
2226
|
+
done < "$TMP/legacy-live"
|
|
1773
2227
|
done < "$TMP/unique-sessions"
|
|
1774
2228
|
[ "$blocked" -eq 0 ] || die 'live references retained; restore registered consumers before cleaning (no subjects removed)'
|
|
1775
2229
|
}
|
|
2230
|
+
# The registry supplies recorded consumers as well as registered sessions. Both list
|
|
2231
|
+
# and expired clean use exactly this offline context and the same decision function.
|
|
2232
|
+
expiry_context() {
|
|
2233
|
+
local repo session cache actual
|
|
2234
|
+
scratch
|
|
2235
|
+
registry expiry-inputs "$@" > "$TMP/expiry-inputs"
|
|
2236
|
+
if [ -n "${EXPIRY_CONSUMER:-}" ]; then printf '%s\t%s\n' "$EXPIRY_CONSUMER" "$(session_path "$EXPIRY_CONSUMER")" >> "$TMP/expiry-inputs"; fi
|
|
2237
|
+
: > "$TMP/expiry-live"; : > "$TMP/expiry-compositions"
|
|
2238
|
+
printf '%s\n' "$NUGET_CACHE" "$HOME/.nuget/packages" > "$TMP/expiry-caches"
|
|
2239
|
+
while IFS= read -r actual; do
|
|
2240
|
+
session=${actual#*$'\t'}; repo=${actual%%$'\t'*}
|
|
2241
|
+
validate_session "$session"
|
|
2242
|
+
if [ -e "$session/nuget-cache" ] || [ -L "$session/nuget-cache" ]; then
|
|
2243
|
+
cache=$(registered_private_cache "$session")
|
|
2244
|
+
printf '%s\n' "$cache" >> "$TMP/expiry-caches"
|
|
2245
|
+
fi
|
|
2246
|
+
if [ -e "$session/composition" ] || [ -L "$session/composition" ]; then
|
|
2247
|
+
composition clean-members all "$session" >> "$TMP/expiry-compositions"
|
|
2248
|
+
fi
|
|
2249
|
+
[ -n "$repo" ] && [ -d "$repo" ] || continue
|
|
2250
|
+
actual=$(git -C "$repo" rev-parse --show-toplevel 2>/dev/null) || continue
|
|
2251
|
+
[ "$actual" = "$repo" ] || continue
|
|
2252
|
+
LIVE_SESSION=$session live_stamps "$repo" >> "$TMP/expiry-live"
|
|
2253
|
+
done < "$TMP/expiry-inputs"
|
|
2254
|
+
}
|
|
2255
|
+
expired_guard() {
|
|
2256
|
+
local stamp=$1 found file
|
|
2257
|
+
expiry_context expiry
|
|
2258
|
+
if grep -Fxq -- "$stamp" "$TMP/expiry-compositions"; then die "expired stamp changed: active composition member: $stamp"; fi
|
|
2259
|
+
while IFS=$'\t' read -r found file; do
|
|
2260
|
+
[ "$found" != "$stamp" ] || die "expired stamp changed: live reference in $file"
|
|
2261
|
+
done < "$TMP/expiry-live"
|
|
2262
|
+
}
|
|
2263
|
+
subject_stamp() {
|
|
2264
|
+
local name=${1##*/}
|
|
2265
|
+
if [[ "$name" =~ ([0-9]+\.[0-9]+\.[0-9]+-(local\.[0-9]{12}|train)\.g[0-9a-f]{7}) ]]; then printf '%s\n' "${BASH_REMATCH[1]}"; fi
|
|
2266
|
+
}
|
|
1776
2267
|
clean() {
|
|
1777
|
-
local version='' apply=0 consumer='' arg path cache image session maximum count=0 rel=''
|
|
1778
|
-
local
|
|
2268
|
+
local version='' apply=0 consumer='' arg path cache image session maximum count=0 rel='' private=0 whole_cache='' whole_session=''
|
|
2269
|
+
local expired=0 online=0 selected_stamp decision reason EXPIRY_CONSUMER=''
|
|
2270
|
+
local subjects=() kinds=() caches=("$NUGET_CACHE") cache_sessions=() filtered_subjects=() filtered_kinds=()
|
|
1779
2271
|
while [ $# -gt 0 ]; do
|
|
1780
2272
|
arg=$1; shift
|
|
1781
2273
|
case "$arg" in
|
|
1782
2274
|
--stamp) [ $# -gt 0 ] || die '--stamp requires a stamp'; [ -z "$version" ] || die 'choose --stamp or --all'; version=$1; shift; valid_stamp "$version" ;;
|
|
1783
2275
|
--all) [ -z "$version" ] || die 'choose --stamp or --all'; version=all ;;
|
|
2276
|
+
--expired) expired=1 ;;
|
|
2277
|
+
--online) online=1 ;;
|
|
1784
2278
|
--apply) apply=1 ;;
|
|
2279
|
+
--private-cache) private=1 ;;
|
|
1785
2280
|
--dir) [ $# -gt 0 ] || die '--dir requires a relative directory'; rel=${1%/}; shift ;;
|
|
1786
2281
|
-*) die "unknown clean option: $arg" ;;
|
|
1787
2282
|
*) [ -z "$consumer" ] || die 'clean takes at most one consumer'; consumer=$(repo_root "$arg") ;;
|
|
1788
2283
|
esac
|
|
1789
2284
|
done
|
|
2285
|
+
[ "$expired" -eq 0 ] || [ -z "$version" ] || die '--expired cannot be combined with --stamp or --all'
|
|
2286
|
+
[ "$online" -eq 0 ] || [ "$expired" -eq 1 ] || die '--online requires --expired'
|
|
2287
|
+
[ "$expired" -eq 0 ] || [ "$private" -eq 0 ] || die '--expired cannot remove a whole private cache'
|
|
1790
2288
|
local registered
|
|
1791
|
-
if [ -n "$consumer" ]; then
|
|
2289
|
+
if [ "$expired" -eq 0 ] && [ -n "$consumer" ]; then
|
|
1792
2290
|
session=$(session_path "$consumer")
|
|
1793
2291
|
if [ -e "$session/composition" ] || [ -L "$session/composition" ]; then composition clean-guard "${version:-all}" "$session"; fi
|
|
1794
2292
|
fi
|
|
1795
|
-
if [ -f "$FEED/sessions" ]; then
|
|
2293
|
+
if [ "$expired" -eq 0 ] && [ -f "$FEED/sessions" ]; then
|
|
1796
2294
|
while IFS= read -r registered; do
|
|
1797
2295
|
if [ -e "$registered/composition" ] || [ -L "$registered/composition" ]; then
|
|
1798
2296
|
composition clean-guard "${version:-all}"
|
|
@@ -1805,6 +2303,37 @@ clean() {
|
|
|
1805
2303
|
[ ${#maximum} -le 6 ] || die 'CRATIS_LOCAL_CLEAN_MAX is too large'
|
|
1806
2304
|
maximum=$((10#$maximum))
|
|
1807
2305
|
scratch
|
|
2306
|
+
if [ -f "$FEED/sessions" ]; then cat "$FEED/sessions" > "$TMP/sessions"; else : > "$TMP/sessions"; fi
|
|
2307
|
+
if [ -n "$consumer" ]; then session_path "$consumer" >> "$TMP/sessions"; fi
|
|
2308
|
+
sort -u "$TMP/sessions" > "$TMP/unique-sessions"
|
|
2309
|
+
if [ "$expired" -eq 1 ]; then
|
|
2310
|
+
EXPIRY_CONSUMER=$consumer
|
|
2311
|
+
if [ "$online" -eq 1 ]; then registry expiry --online > "$TMP/expiry-decisions"; else registry expiry > "$TMP/expiry-decisions"; fi
|
|
2312
|
+
: > "$TMP/expired-stamps"
|
|
2313
|
+
while IFS=$'\t' read -r decision selected_stamp reason; do
|
|
2314
|
+
printf '%s %s: %s\n' "$decision" "$selected_stamp" "$reason"
|
|
2315
|
+
if [ "$decision" = expired ]; then printf '%s\n' "$selected_stamp" >> "$TMP/expired-stamps"; fi
|
|
2316
|
+
done < "$TMP/expiry-decisions"
|
|
2317
|
+
while IFS= read -r path; do printf '%s\n' "${path#*$'\t'}" >> "$TMP/sessions"; done < "$TMP/expiry-inputs"
|
|
2318
|
+
sort -u "$TMP/sessions" > "$TMP/unique-sessions"
|
|
2319
|
+
caches+=("$HOME/.nuget/packages")
|
|
2320
|
+
fi
|
|
2321
|
+
while IFS= read -r session; do
|
|
2322
|
+
if [ -e "$session/nuget-cache" ] || [ -L "$session/nuget-cache" ]; then
|
|
2323
|
+
cache=$(registered_private_cache "$session")
|
|
2324
|
+
cache_sessions+=("$session"); caches+=("$cache")
|
|
2325
|
+
fi
|
|
2326
|
+
done < "$TMP/unique-sessions"
|
|
2327
|
+
if [ "$private" -eq 1 ]; then
|
|
2328
|
+
[ -n "$consumer" ] || die 'clean --private-cache requires a consumer'
|
|
2329
|
+
whole_session=$(session_path "$consumer")
|
|
2330
|
+
[ -f "$whole_session/nuget-cache" ] || die "no registered private cache: $consumer"
|
|
2331
|
+
whole_cache=$(registered_private_cache "$whole_session")
|
|
2332
|
+
private_cache_restored "$whole_session"
|
|
2333
|
+
clean_live_references ''
|
|
2334
|
+
subjects+=("$whole_cache"); kinds+=(private-cache)
|
|
2335
|
+
fi
|
|
2336
|
+
printf '%s\n' "${caches[@]}" | sort -u > "$TMP/nuget-caches"
|
|
1808
2337
|
for cache in "$FEED/nuget" "$FEED/npm"; do
|
|
1809
2338
|
[ -d "$cache" ] || continue
|
|
1810
2339
|
while IFS= read -r -d '' path; do
|
|
@@ -1813,8 +2342,10 @@ clean() {
|
|
|
1813
2342
|
subjects+=("$path"); kinds+=(file)
|
|
1814
2343
|
done < <(find "$cache" -type f \( -name '*.nupkg' -o -name '*.snupkg' -o -name '*.tgz' \) -print0)
|
|
1815
2344
|
done
|
|
1816
|
-
|
|
1817
|
-
|
|
2345
|
+
while IFS= read -r path; do
|
|
2346
|
+
[ "$path" != "$whole_cache" ] || continue
|
|
2347
|
+
[ -d "$path" ] || continue
|
|
2348
|
+
for cache in "$path"/*; do
|
|
1818
2349
|
[ -d "$cache" ] && [ ! -L "$cache" ] || continue
|
|
1819
2350
|
for path in "$cache"/*-local.* "$cache"/*-train.*; do
|
|
1820
2351
|
[ -d "$path" ] && [ ! -L "$path" ] || continue
|
|
@@ -1823,7 +2354,7 @@ clean() {
|
|
|
1823
2354
|
subjects+=("$path"); kinds+=(directory)
|
|
1824
2355
|
done
|
|
1825
2356
|
done
|
|
1826
|
-
|
|
2357
|
+
done < "$TMP/nuget-caches"
|
|
1827
2358
|
if [ -n "$consumer" ] && [ ! -f "$consumer${rel:+/$rel}/package.json" ]; then
|
|
1828
2359
|
printf 'SKIP: no package.json in %s; pass --dir REL for a Yarn project in a subdirectory\n' "$consumer${rel:+/$rel}" >&2
|
|
1829
2360
|
elif command -v yarn >/dev/null 2>&1 && [ -n "$consumer" ]; then
|
|
@@ -1849,12 +2380,10 @@ clean() {
|
|
|
1849
2380
|
printf 'SKIP: Docker unavailable or did not respond within 10 seconds\n' >&2
|
|
1850
2381
|
fi
|
|
1851
2382
|
else printf 'SKIP: docker is not on PATH\n' >&2; fi
|
|
1852
|
-
if [ -f "$FEED/sessions" ]; then cat "$FEED/sessions" > "$TMP/sessions"; else : > "$TMP/sessions"; fi
|
|
1853
|
-
if [ -n "$consumer" ]; then session_path "$consumer" >> "$TMP/sessions"; fi
|
|
1854
|
-
sort -u "$TMP/sessions" > "$TMP/unique-sessions"
|
|
1855
2383
|
while IFS= read -r session; do
|
|
1856
2384
|
[ -d "$session" ] && [ -f "$session/repo" ] && [ -f "$session/stamp" ] || continue
|
|
1857
2385
|
local_artifact "$(cat "$session/stamp")" || continue
|
|
2386
|
+
if [ "$expired" -eq 1 ] && ! grep -Fxq -- "$(cat "$session/stamp")" "$TMP/expired-stamps"; then continue; fi
|
|
1858
2387
|
if [ -n "$version" ] && [ "$version" != all ] && [ "$(cat "$session/stamp")" != "$version" ]; then continue; fi
|
|
1859
2388
|
[ ! -L "$session" ] && [ "$session" != / ] || die "unsafe session: $session"
|
|
1860
2389
|
validate_session "$session"
|
|
@@ -1863,25 +2392,55 @@ clean() {
|
|
|
1863
2392
|
[ ! -d "$session/npm-backup" ] || die "unresolved npm backup: $session; restore before cleaning"
|
|
1864
2393
|
subjects+=("$session"); kinds+=(session)
|
|
1865
2394
|
done < "$TMP/unique-sessions"
|
|
1866
|
-
|
|
2395
|
+
# Stamp records exist only after a registry-aware pack or use; without any, clean needs no node.
|
|
2396
|
+
: > "$TMP/stamp-records"
|
|
2397
|
+
if [ -e "$FEED/stamps" ] || [ -L "$FEED/stamps" ]; then registry clean "$version" > "$TMP/stamp-records"; fi
|
|
2398
|
+
while IFS= read -r path; do subjects+=("$path"); kinds+=(stamp-record); done < "$TMP/stamp-records"
|
|
2399
|
+
if [ "$expired" -eq 1 ]; then
|
|
2400
|
+
for ((arg=0; arg<${#subjects[@]}; arg++)); do
|
|
2401
|
+
if [ "${kinds[$arg]}" = session ]; then selected_stamp=$(cat "${subjects[$arg]}/stamp"); else selected_stamp=$(subject_stamp "${subjects[$arg]}"); fi
|
|
2402
|
+
if grep -Fxq -- "$selected_stamp" "$TMP/expired-stamps"; then
|
|
2403
|
+
filtered_subjects+=("${subjects[$arg]}"); filtered_kinds+=("${kinds[$arg]}")
|
|
2404
|
+
fi
|
|
2405
|
+
done
|
|
2406
|
+
subjects=(${filtered_subjects[@]+"${filtered_subjects[@]}"}); kinds=(${filtered_kinds[@]+"${filtered_kinds[@]}"})
|
|
2407
|
+
else clean_live_references "$version"; fi
|
|
1867
2408
|
count=${#subjects[@]}
|
|
1868
2409
|
[ "$count" -le "$maximum" ] || die "clean fuse: $count subjects exceeds $maximum"
|
|
1869
2410
|
for ((arg=0; arg<count; arg++)); do printf '%s: %s\n' "${kinds[$arg]}" "${subjects[$arg]}"; done
|
|
1870
2411
|
if [ "$apply" -eq 1 ] && [ "$DRY" -eq 0 ]; then
|
|
1871
2412
|
for ((arg=0; arg<count; arg++)); do
|
|
1872
|
-
|
|
2413
|
+
if [ "$expired" -eq 1 ]; then
|
|
2414
|
+
if [ "${kinds[$arg]}" = session ]; then selected_stamp=$(cat "${subjects[$arg]}/stamp"); else selected_stamp=$(subject_stamp "${subjects[$arg]}"); fi
|
|
2415
|
+
expired_guard "$selected_stamp"
|
|
2416
|
+
else clean_live_references "$version"; fi
|
|
2417
|
+
for session in ${cache_sessions[@]+"${cache_sessions[@]}"}; do registered_private_cache "$session" > /dev/null; done
|
|
1873
2418
|
path=${subjects[$arg]}
|
|
1874
2419
|
case "${kinds[$arg]}" in
|
|
2420
|
+
private-cache)
|
|
2421
|
+
[ "$(registered_private_cache "$whole_session")" = "$path" ] || die "private cache changed: $path"
|
|
2422
|
+
private_cache_restored "$whole_session"
|
|
2423
|
+
clean_live_references ''
|
|
2424
|
+
rm -rf -- "$path"
|
|
2425
|
+
rm -- "$whole_session/nuget-cache"
|
|
2426
|
+
# Later subjects still validate all remaining registrations.
|
|
2427
|
+
cache_sessions=()
|
|
2428
|
+
while IFS= read -r session; do
|
|
2429
|
+
if [ -f "$session/nuget-cache" ]; then cache_sessions+=("$session"); fi
|
|
2430
|
+
done < "$TMP/unique-sessions"
|
|
2431
|
+
;;
|
|
2432
|
+
stamp-record) registry_update remove "${path##*/}" ;;
|
|
1875
2433
|
image) docker rmi "$path" ;;
|
|
1876
2434
|
file) [ -f "$path" ] && [ ! -L "$path" ] || die "clean subject changed: $path"; rm -- "$path" ;;
|
|
1877
2435
|
directory) [ -d "$path" ] && [ ! -L "$path" ] || die "clean subject changed: $path"; rm -rf -- "$path" ;;
|
|
1878
2436
|
session)
|
|
1879
2437
|
[ -f "$path/stamp" ] && [ ! -L "$path" ] && [ ! -e "$path/npm-backup" ] || die "session changed: $path"
|
|
1880
2438
|
validate_session "$path"
|
|
1881
|
-
rm -f -- "$path/
|
|
1882
|
-
rmdir -- "$path" ;;
|
|
2439
|
+
rm -f -- "$path/stamp" "$path/env" "$path/npm-dir" "$path/Directory.Packages.props" "$path/cratis-local.targets" "$path/nuget.config"
|
|
2440
|
+
if [ ! -f "$path/nuget-cache" ]; then rm -f -- "$path/repo"; rmdir -- "$path"; fi ;;
|
|
1883
2441
|
esac
|
|
1884
2442
|
done
|
|
2443
|
+
if [ "$expired" -eq 1 ]; then registry_update prune; fi
|
|
1885
2444
|
printf 'removed %s subjects\n' "$count"
|
|
1886
2445
|
else printf 'dry-run: %s subjects (use --apply to remove)\n' "$count"; fi
|
|
1887
2446
|
}
|
|
@@ -1907,15 +2466,18 @@ self_test_nuget_overlay() {
|
|
|
1907
2466
|
|
|
1908
2467
|
self_test() {
|
|
1909
2468
|
[ $# -eq 0 ] || die '--self-test takes no arguments'
|
|
1910
|
-
local root home repo feed version output status passed=0 skipped=0 test_key arg map
|
|
2469
|
+
local root home repo feed version output status passed=0 skipped=0 test_key arg map system_path
|
|
1911
2470
|
# Fixtures use the physical path (macOS TMPDIR is /var -> /private/var) because the helper
|
|
1912
2471
|
# reports repositories by their git toplevel; symlinked feeds have their own test below.
|
|
1913
2472
|
scratch; root=$(cd -P "$TMP" && pwd -P)/self-test; mkdir -p "$root/home" "$root/repo"
|
|
1914
2473
|
map=$root/map.tsv; home=$root/home; repo=$root/repo; feed=$home/.cache/cratis/local-feed
|
|
1915
|
-
#
|
|
1916
|
-
# Both isolate HOME/config and omit the host adapter.
|
|
1917
|
-
|
|
1918
|
-
|
|
2474
|
+
# Isolate tool discovery, adding only node for the stamp registry. npm tests
|
|
2475
|
+
# inherit PATH. Both isolate HOME/config and omit the host adapter.
|
|
2476
|
+
need node; mkdir -p "$root/registry-bin"
|
|
2477
|
+
ln -s "$(command -v node)" "$root/registry-bin/node"
|
|
2478
|
+
system_path=$root/registry-bin:/usr/bin:/bin:/usr/sbin:/sbin
|
|
2479
|
+
st() { env -i HOME="$home" PATH="$system_path" TMPDIR="$TMP" /bin/bash "$SELF" "$@"; }
|
|
2480
|
+
stm() { env -i HOME="$home" PATH="$system_path" TMPDIR="$TMP" CRATIS_LOCAL_MAP="$map" /bin/bash "$SELF" "$@"; }
|
|
1919
2481
|
has() { grep -Fq -- "$1" "$2"; }
|
|
1920
2482
|
st_npm() { env -i HOME="$home" PATH="$PATH" TMPDIR="$TMP" /bin/bash "$SELF" "$@"; }
|
|
1921
2483
|
git -C "$repo" init -q
|
|
@@ -1991,6 +2553,14 @@ LOCK
|
|
|
1991
2553
|
passed=$((passed + 5))
|
|
1992
2554
|
st check "$repo" > "$root/output" || failed 'self-test: clean repo'
|
|
1993
2555
|
passed=$((passed + 1))
|
|
2556
|
+
# Legacy clean of a feed without stamp records must not need node.
|
|
2557
|
+
mkdir -p "$root/no-registry-feed/nuget"
|
|
2558
|
+
if PATH=/usr/bin:/bin:/usr/sbin:/sbin command -v node > /dev/null 2>&1; then
|
|
2559
|
+
printf 'SKIP: clean without node needs a system PATH without node\n' >&2; skipped=$((skipped + 1))
|
|
2560
|
+
else
|
|
2561
|
+
expect_status 0 'legacy clean without node or stamp records' env -i HOME="$home" PATH=/usr/bin:/bin:/usr/sbin:/sbin TMPDIR="$TMP" CRATIS_LOCAL_FEED="$root/no-registry-feed" /bin/bash "$SELF" clean
|
|
2562
|
+
passed=$((passed + 1))
|
|
2563
|
+
fi
|
|
1994
2564
|
for test_key in stamp npm props sources targets previous override feed; do
|
|
1995
2565
|
case "$test_key" in
|
|
1996
2566
|
stamp) printf '%s\n' "$version" > "$repo/probe" ;;
|
|
@@ -2197,6 +2767,8 @@ NODE
|
|
|
2197
2767
|
passed=$((passed + 6))
|
|
2198
2768
|
else printf 'SKIP: use-npm --dir needs node\n'; skipped=$((skipped + 6)); fi
|
|
2199
2769
|
self_test_live_clean
|
|
2770
|
+
self_test_private_cache
|
|
2771
|
+
self_test_registry
|
|
2200
2772
|
mkdir -p "$home/.nuget/packages/cratis.test/$version" "$home/.nuget/packages/cratis.test/1.2.3"
|
|
2201
2773
|
mkdir -p "$home/.nuget/packages/cratis.test/1.2.3-local.keep"
|
|
2202
2774
|
for arg in nuget npm; do
|
|
@@ -2219,10 +2791,351 @@ NODE
|
|
|
2219
2791
|
[ -d "$home/.nuget/packages/cratis.test/1.2.3" ] || failed 'self-test: clean removed real version'
|
|
2220
2792
|
passed=$((passed + 5))
|
|
2221
2793
|
printf 'SKIP: real dotnet/Yarn/Docker packing is not a hermetic self-test\n'; skipped=$((skipped + 3))
|
|
2794
|
+
self_test_expired
|
|
2222
2795
|
self_test_matrix
|
|
2223
2796
|
printf 'self-test: %s passed, %s skipped\n' "$passed" "$skipped"
|
|
2224
2797
|
}
|
|
2225
2798
|
|
|
2799
|
+
self_test_expired() {
|
|
2800
|
+
local fixture ef er es ec record result expected stamp=9.8.7-train.gabcdef0
|
|
2801
|
+
local fixtures=('task finished' 'consumers gone' 'released offline cache' 'live reference kept' 'composition kept' 'unrecorded kept' 'dry-run no mutation' 'with stamp' 'fuse' 'online unreachable kept' 'prunes sessions' 'list json expiry' 'invalid record' 'record consumer reference kept' 'private release cache' 'prerelease cache kept' 'older release kept' 'without task kept' 'with all')
|
|
2802
|
+
ext() { env -i HOME="$ef/home" PATH="$system_path" TMPDIR="$TMP" CRATIS_LOCAL_FEED="$ef/feed" NUGET_PACKAGES="$ec" CRATIS_LOCAL_CLEAN_MAX="${limit:-500}" CRATIS_LOCAL_RELEASE_URL_BASE=http://127.0.0.1:1 /bin/bash "$SELF" "$@"; }
|
|
2803
|
+
for fixture in "${fixtures[@]}"; do
|
|
2804
|
+
ef=$root/expired-${fixture// /-}; er=$ef/repo; ec=$ef/cache
|
|
2805
|
+
mkdir -p "$er" "$ef/feed/nuget" "$ec/cratis.expiry/$stamp" "$ef/home"
|
|
2806
|
+
git -C "$er" init -q
|
|
2807
|
+
es=$(git -C "$er" rev-parse --absolute-git-dir)/cratis-local
|
|
2808
|
+
record=$ef/feed/stamps/$stamp/record.json
|
|
2809
|
+
mkdir -p "${record%/*}"
|
|
2810
|
+
printf 'fixture\n' > "$ef/feed/nuget/Cratis.Expiry.$stamp.nupkg"
|
|
2811
|
+
node - "$record" "$stamp" "$er" "$es" "$fixture" <<'NODE'
|
|
2812
|
+
const fs = require('node:fs'); const [file,stamp,repo,session,fixture] = process.argv.slice(2), now = new Date().toISOString();
|
|
2813
|
+
const finished = ['task finished','live reference kept','composition kept','dry-run no mutation','fuse','prunes sessions','list json expiry','record consumer reference kept'].includes(fixture);
|
|
2814
|
+
const r = {schema:1,stamp,base:'9.8.7',kind:'train',created:now,updated:now,
|
|
2815
|
+
packs:[{ecosystem:'nuget',ids:['Cratis.Expiry'],producer:{repo:'Fixture',worktree:repo,branch:'main',head:'a'.repeat(40)},task:'Fixture#1',time:now}],
|
|
2816
|
+
consumers:[],tasks:{'Fixture#1':{finished:finished ? now : null}}};
|
|
2817
|
+
if (fixture === 'consumers gone') r.consumers.push({worktree:repo + '/gone',session:session + '-gone',ecosystem:'nuget',task:null,time:now});
|
|
2818
|
+
if (fixture === 'record consumer reference kept') r.consumers.push({worktree:repo,session,ecosystem:'nuget',task:null,time:now});
|
|
2819
|
+
if (fixture === 'without task kept') r.packs[0].task = null;
|
|
2820
|
+
fs.writeFileSync(file,JSON.stringify(r) + '\n');
|
|
2821
|
+
NODE
|
|
2822
|
+
expected=0; result=removed
|
|
2823
|
+
case "$fixture" in
|
|
2824
|
+
'released offline cache') mkdir -p "$ec/cratis.expiry/9.8.8"; : > "$ec/cratis.expiry/9.8.8/.nupkg.metadata" ;;
|
|
2825
|
+
'private release cache')
|
|
2826
|
+
mkdir -p "$es" "$er/.git/cratis-local-nuget/cratis.expiry/9.8.7"
|
|
2827
|
+
printf '%s\n' "$er" > "$es/repo"
|
|
2828
|
+
printf '%s\n' "$es" > "$ef/feed/sessions"
|
|
2829
|
+
printf '%s\n' "$er/.git/cratis-local-nuget" > "$es/nuget-cache"
|
|
2830
|
+
printf '%s\n' "$er" > "$er/.git/cratis-local-nuget/.cratis-local-cache"
|
|
2831
|
+
: > "$er/.git/cratis-local-nuget/cratis.expiry/9.8.7/.nupkg.metadata" ;;
|
|
2832
|
+
'live reference kept'|'record consumer reference kept')
|
|
2833
|
+
mkdir -p "$es"; printf '%s\n' "$er" > "$es/repo"
|
|
2834
|
+
if [ "$fixture" = 'live reference kept' ]; then printf '%s\n' "$es" > "$ef/feed/sessions"; fi
|
|
2835
|
+
printf '{"dependencies":{"fixture":"%s"}}\n' "$stamp" > "$er/package.json"
|
|
2836
|
+
result=kept ;;
|
|
2837
|
+
'composition kept')
|
|
2838
|
+
mkdir -p "$es/composition"; printf '%s\n' "$er" > "$es/repo"
|
|
2839
|
+
printf '%s\n' "$es" > "$ef/feed/sessions"
|
|
2840
|
+
printf 'Fixture\tCratis.Expiry\t%s\t%s\n' "$stamp" "$er" > "$es/composition/manifest.tsv"
|
|
2841
|
+
result=kept ;;
|
|
2842
|
+
'unrecorded kept') rm -- "$record"; rmdir "${record%/*}"; result=kept ;;
|
|
2843
|
+
'dry-run no mutation') cp "$record" "$ef/before"; result=kept ;;
|
|
2844
|
+
'with stamp'|'with all'|'fuse'|'invalid record')
|
|
2845
|
+
expected=2; result=kept
|
|
2846
|
+
if [ "$fixture" = 'invalid record' ]; then printf '{broken\n' > "$record"; fi ;;
|
|
2847
|
+
'prerelease cache kept') mkdir -p "$ec/cratis.expiry/9.8.9-preview"; : > "$ec/cratis.expiry/9.8.9-preview/.nupkg.metadata"; result=kept ;;
|
|
2848
|
+
'older release kept') mkdir -p "$ec/cratis.expiry/9.8.6"; : > "$ec/cratis.expiry/9.8.6/.nupkg.metadata"; result=kept ;;
|
|
2849
|
+
'online unreachable kept'|'without task kept') result=kept ;;
|
|
2850
|
+
'list json expiry') result=kept ;;
|
|
2851
|
+
'prunes sessions')
|
|
2852
|
+
mkdir -p "$es"; printf '%s\n' "$er" > "$es/repo"; : > "$es/env"
|
|
2853
|
+
printf '%s\n%s\n' "$ef/missing-session" "$es" > "$ef/feed/sessions"
|
|
2854
|
+
node - "$record" "$es" <<'NODE'
|
|
2855
|
+
const fs = require('node:fs'); const [file,session] = process.argv.slice(2), r = JSON.parse(fs.readFileSync(file));
|
|
2856
|
+
r.stamp = '9.8.7-train.g1234567'; r.packs[0].task = null;
|
|
2857
|
+
r.consumers = [{worktree:r.packs[0].producer.worktree,session,ecosystem:'nuget',task:null,time:r.created},
|
|
2858
|
+
{worktree:'/nonexistent/cratis-expiry-fixture',session:session + '-gone',ecosystem:'nuget',task:null,time:r.created},
|
|
2859
|
+
{worktree:r.packs[0].producer.worktree,session:session + '-missing',ecosystem:'nuget',task:null,time:r.created}];
|
|
2860
|
+
const target = file.replace('gabcdef0','g1234567'); fs.mkdirSync(require('node:path').dirname(target)); fs.writeFileSync(target,JSON.stringify(r));
|
|
2861
|
+
NODE
|
|
2862
|
+
;;
|
|
2863
|
+
esac
|
|
2864
|
+
case "$fixture" in
|
|
2865
|
+
'with stamp') expect_status "$expected" 'expired with stamp' ext clean --expired --stamp "$stamp" --apply ;;
|
|
2866
|
+
'with all') expect_status "$expected" 'expired with all' ext clean --all --expired --apply ;;
|
|
2867
|
+
'fuse') limit=0 expect_status "$expected" 'expired fuse' ext clean --expired --apply ;;
|
|
2868
|
+
'online unreachable kept') expect_status 0 'expired online unreachable kept' ext clean --expired --online --apply ;;
|
|
2869
|
+
'dry-run no mutation') expect_status 0 'expired dry-run no mutation' ext clean --expired ;;
|
|
2870
|
+
'list json expiry')
|
|
2871
|
+
expect_status 0 'list json expiry' ext list --json
|
|
2872
|
+
node - "$root/output" <<'NODE' || failed 'list json expiry decision'
|
|
2873
|
+
const r = JSON.parse(require('node:fs').readFileSync(process.argv[2])).stamps;
|
|
2874
|
+
if (r.length !== 1 || !r[0].expiry.expired || r[0].expiry.reasons.join() !== 'task-finished' || r[0].expiry.kept.length) process.exit(1);
|
|
2875
|
+
NODE
|
|
2876
|
+
;;
|
|
2877
|
+
*) expect_status "$expected" "expired $fixture" ext clean --expired --apply ;;
|
|
2878
|
+
esac
|
|
2879
|
+
if [ "$result" = removed ]; then
|
|
2880
|
+
[ ! -e "$record" ] && [ ! -e "$ef/feed/nuget/Cratis.Expiry.$stamp.nupkg" ] && [ ! -e "$ec/cratis.expiry/$stamp" ] || failed "expired $fixture left subjects"
|
|
2881
|
+
else
|
|
2882
|
+
[ -f "$ef/feed/nuget/Cratis.Expiry.$stamp.nupkg" ] && [ -d "$ec/cratis.expiry/$stamp" ] || failed "expired $fixture removed subjects"
|
|
2883
|
+
fi
|
|
2884
|
+
case "$fixture" in
|
|
2885
|
+
'task finished'|'consumers gone') grep -Fq "expired $stamp: ${fixture// /-}" "$root/output" || failed "expired $fixture reason" ;;
|
|
2886
|
+
'released offline cache') grep -Fq 'released:Cratis.Expiry@9.8.8' "$root/output" || failed 'offline release reason' ;;
|
|
2887
|
+
'live reference kept'|'record consumer reference kept') grep -Fq "kept $stamp: live reference in $er/package.json" "$root/output" || failed 'live override reason' ;;
|
|
2888
|
+
'composition kept') grep -Fq "kept $stamp: active composition member" "$root/output" || failed 'composition override reason' ;;
|
|
2889
|
+
'unrecorded kept') grep -Fq "unrecorded; use clean --stamp $stamp" "$root/output" || failed 'unrecorded reason' ;;
|
|
2890
|
+
'online unreachable kept') grep -Fq "kept $stamp: active" "$root/output" || failed 'network failure removed active stamp' ;;
|
|
2891
|
+
'dry-run no mutation') cmp -s "$record" "$ef/before" || failed 'expired dry-run rewrote record' ;;
|
|
2892
|
+
'prunes sessions')
|
|
2893
|
+
[ "$(cat "$ef/feed/sessions")" = "$es" ] || failed 'expired prunes sessions'
|
|
2894
|
+
node - "$ef/feed/stamps/9.8.7-train.g1234567/record.json" <<'NODE' || failed 'expired prunes consumers'
|
|
2895
|
+
// The consumer with a missing session but an existing worktree stays, so its manifests remain protected.
|
|
2896
|
+
const r = JSON.parse(require('node:fs').readFileSync(process.argv[2])); if (r.consumers.length !== 2 || r.consumers.some(c => c.worktree.endsWith('fixture')) || !r.consumers.some(c => c.session.endsWith('-missing'))) process.exit(1);
|
|
2897
|
+
NODE
|
|
2898
|
+
;;
|
|
2899
|
+
esac
|
|
2900
|
+
done
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
self_test_registry() {
|
|
2904
|
+
local rr=$root/'registry repo' rf=$root/registry-feed rb=$root/registry-tools rs record
|
|
2905
|
+
local rt=Cratis/Fixture#1 legacy=1.2.5-train.gabcdef0
|
|
2906
|
+
mkdir -p "$rr" "$rf/nuget" "$rb"
|
|
2907
|
+
git -C "$rr" init -q
|
|
2908
|
+
cp "$repo/Directory.Packages.props" "$rr/Directory.Packages.props"
|
|
2909
|
+
printf '{"name":"@cratis/registry","version":"1.0.0"}\n' > "$rr/package.json"
|
|
2910
|
+
git -C "$rr" add .
|
|
2911
|
+
git -C "$rr" -c user.email=test@example.invalid -c user.name='Registry fixture' commit -qm initial
|
|
2912
|
+
rs=$(git -C "$rr" rev-parse --absolute-git-dir)/cratis-local
|
|
2913
|
+
record=$rf/stamps/$version/record.json
|
|
2914
|
+
# Pack-tool doubles exercise successful/failed pack boundaries without building.
|
|
2915
|
+
cat > "$rb/dotnet" <<'SH'
|
|
2916
|
+
#!/bin/bash
|
|
2917
|
+
set -eu
|
|
2918
|
+
[ ! -e "$CRATIS_LOCAL_FEED/fail-pack" ] || exit 1
|
|
2919
|
+
version=''; output=''
|
|
2920
|
+
for arg in "$@"; do
|
|
2921
|
+
case "$arg" in -p:PackageVersion=*) version=${arg#*=} ;; esac
|
|
2922
|
+
if [ "$output" = next ]; then output=$arg; fi
|
|
2923
|
+
if [ "$arg" = -o ]; then output=next; fi
|
|
2924
|
+
done
|
|
2925
|
+
printf 'packed\n' > "$output/Cratis.Registry.$version.nupkg"
|
|
2926
|
+
SH
|
|
2927
|
+
printf '#!/bin/sh\nprintf '\''{"location":"."}\\n'\''\n' > "$rb/yarn"
|
|
2928
|
+
cat > "$rb/npm" <<'SH'
|
|
2929
|
+
#!/bin/bash
|
|
2930
|
+
set -eu
|
|
2931
|
+
version=$(node -p 'require("./package.json").version')
|
|
2932
|
+
mkdir -p "$TMPDIR/registry-tar/package"
|
|
2933
|
+
cp package.json "$TMPDIR/registry-tar/package/package.json"
|
|
2934
|
+
tar -czf "$CRATIS_LOCAL_FEED/npm/cratis-registry-$version.tgz" -C "$TMPDIR/registry-tar" package
|
|
2935
|
+
SH
|
|
2936
|
+
chmod +x "$rb/dotnet" "$rb/npm" "$rb/yarn"
|
|
2937
|
+
rgt() { env -i HOME="$home" PATH="$rb:$system_path" TMPDIR="$TMP" CRATIS_LOCAL_FEED="$rf" NUGET_PACKAGES="$root/registry-cache" CRATIS_LOCAL_TASK="$rt" /bin/bash "$SELF" "$@"; }
|
|
2938
|
+
rg_assert() { node - "$record" "$@" <<'NODE'
|
|
2939
|
+
const fs = require('node:fs'); const [file,kind,version,repo,session] = process.argv.slice(2);
|
|
2940
|
+
const r = JSON.parse(fs.readFileSync(file,'utf8'));
|
|
2941
|
+
if (r.schema !== 1 || r.stamp !== version || !r.created.endsWith('Z') || !r.updated.endsWith('Z')) process.exit(1);
|
|
2942
|
+
if (kind === 'pack' && (r.packs.length !== 2 || !r.packs.some(p => p.ecosystem === 'nuget' && p.ids.join() === 'Cratis.Registry') || !r.packs.some(p => p.ids.includes('@cratis/registry')) || r.packs.some(p => p.producer.worktree !== repo || !/^[0-9a-f]{40}$/.test(p.producer.head)) || (fs.statSync(file).mode & 0o777) !== 0o600)) process.exit(1);
|
|
2943
|
+
if (kind === 'use' && (r.consumers.length !== 2 || r.consumers.some(c => c.worktree !== repo || c.session !== session || c.task !== 'Cratis/Fixture#2'))) process.exit(1);
|
|
2944
|
+
if (kind === 'restored' && r.consumers.some(c => !c.restored?.endsWith('Z'))) process.exit(1);
|
|
2945
|
+
if (kind === 'finished' && !r.tasks['Cratis/Fixture#1'].finished?.endsWith('Z')) process.exit(1);
|
|
2946
|
+
NODE
|
|
2947
|
+
}
|
|
2948
|
+
: > "$rf/nuget/Cratis.Previous.$version.nupkg"
|
|
2949
|
+
rgt pack-nuget "$rr" "$version" Fixture.csproj > "$root/output"
|
|
2950
|
+
rgt pack-npm "$rr" "$version" > "$root/output"
|
|
2951
|
+
expect_status 0 'registry pack records' rg_assert pack "$version" "$rr" "$rs"
|
|
2952
|
+
rgt use-nuget "$rr" "$version" --task Cratis/Fixture#2 > "$root/output"
|
|
2953
|
+
rgt use-npm "$rr" "$version" --task Cratis/Fixture#2 > "$root/output"
|
|
2954
|
+
expect_status 0 'registry use consumer' rg_assert use "$version" "$rr" "$rs"
|
|
2955
|
+
expect_status 0 'registry restore consumer' rgt restore "$rr"
|
|
2956
|
+
rg_assert restored "$version" "$rr" "$rs" || failed 'registry restore history'
|
|
2957
|
+
cp "$record" "$root/registry-before"
|
|
2958
|
+
expect_status 0 'registry dry-run no write' rgt --dry-run pack-nuget "$rr" "$legacy" Fixture.csproj
|
|
2959
|
+
[ ! -e "$rf/stamps/$legacy" ] || failed 'registry dry-run created record'
|
|
2960
|
+
rgt --dry-run use-nuget "$rr" "$version" > "$root/output"
|
|
2961
|
+
cmp -s "$record" "$root/registry-before" || failed 'registry dry-run use rewrote record'
|
|
2962
|
+
expect_status 0 'finish task dry-run' rgt finish --task "$rt"
|
|
2963
|
+
cmp -s "$record" "$root/registry-before" || failed 'finish dry-run rewrote record'
|
|
2964
|
+
expect_status 0 'finish task apply' rgt finish --task "$rt" --apply
|
|
2965
|
+
rg_assert finished "$version" "$rr" "$rs" || failed 'finish did not mark task'
|
|
2966
|
+
expect_status 0 'finish unknown task' rgt finish --task Cratis/Unknown#2 --apply
|
|
2967
|
+
grep -Fq 'no records name task' "$root/output" || failed 'finish unknown task message'
|
|
2968
|
+
: > "$rf/nuget/Cratis.Legacy.$legacy.nupkg"
|
|
2969
|
+
expect_status 0 'list unrecorded legacy' rgt list --stamp "$legacy" --json
|
|
2970
|
+
node -e 'const r=JSON.parse(require("fs").readFileSync(process.argv[1])).stamps;if(r.length!==1||r[0].recorded||r[0].packages[0].id!=="Cratis.Legacy")process.exit(1)' "$root/output" || failed 'list legacy missing'
|
|
2971
|
+
rgt list --json > "$root/registry-list"
|
|
2972
|
+
rg_json() { node - "$root/registry-list" "$rf" <<'NODE'
|
|
2973
|
+
const fs = require('node:fs'); const [file,feed] = process.argv.slice(2), data = JSON.parse(fs.readFileSync(file));
|
|
2974
|
+
if (data.schema !== 1 || data.feed !== feed || data.stamps.length !== 2) process.exit(1);
|
|
2975
|
+
const keys = ['stamp','base','kind','recorded','packages','producers','tasks','consumers','created','ageSeconds','expiry'].sort().join();
|
|
2976
|
+
for (const r of data.stamps) {
|
|
2977
|
+
if (Object.keys(r).sort().join() !== keys || typeof r.expiry.expired !== 'boolean' || !Array.isArray(r.expiry.reasons) || !Array.isArray(r.expiry.kept)) process.exit(1);
|
|
2978
|
+
for (const p of r.packages) if (Object.keys(p).sort().join() !== 'ecosystem,file,id,version') process.exit(1);
|
|
2979
|
+
for (const c of r.consumers) if (c.state !== 'restored') process.exit(1);
|
|
2980
|
+
}
|
|
2981
|
+
NODE
|
|
2982
|
+
}
|
|
2983
|
+
expect_status 0 'list json schema' rg_json
|
|
2984
|
+
cp "$record" "$root/registry-good"
|
|
2985
|
+
printf '{"schema":2}\n' > "$record"
|
|
2986
|
+
cp "$record" "$root/registry-bad"
|
|
2987
|
+
expect_status 2 'registry rejects bad schema' rgt clean --stamp "$version" --apply
|
|
2988
|
+
cmp -s "$record" "$root/registry-bad" || failed 'registry rewrote bad schema'
|
|
2989
|
+
printf '{broken\n' > "$record"
|
|
2990
|
+
expect_status 2 'registry rejects malformed JSON' rgt clean --stamp "$version" --apply
|
|
2991
|
+
cp "$root/registry-good" "$record"
|
|
2992
|
+
mv "$rf/stamps" "$rf/real-stamps"; ln -s "$rf/real-stamps" "$rf/stamps"
|
|
2993
|
+
expect_status 2 'registry symlink refused' rgt finish --task "$rt" --apply
|
|
2994
|
+
rm "$rf/stamps"; mv "$rf/real-stamps" "$rf/stamps"
|
|
2995
|
+
ln -s "$root/registry-good" "$record.next"
|
|
2996
|
+
expect_status 2 'registry next symlink refused' rgt finish --task "$rt" --apply
|
|
2997
|
+
rm "$record.next"
|
|
2998
|
+
expect_status 2 'registry bad task id' rgt pack-nuget "$rr" "$version" --task 'bad task' Fixture.csproj
|
|
2999
|
+
: > "$rf/fail-pack"
|
|
3000
|
+
expect_status 1 'registry failed pack no write' rgt pack-nuget "$rr" "$legacy" Fixture.csproj
|
|
3001
|
+
[ ! -e "$rf/stamps/$legacy" ] || failed 'failed pack wrote record'
|
|
3002
|
+
rm "$rf/fail-pack"
|
|
3003
|
+
# Post-publication failures preserve overlays and report a warning/findings.
|
|
3004
|
+
printf '{"schema":2}\n' > "$record"
|
|
3005
|
+
expect_status 1 'registry use failure keeps overlay' rgt use-nuget "$rr" "$version"
|
|
3006
|
+
{ [ -f "$rs/env" ] && grep -Fq WARNING "$root/stderr"; } || failed 'registry failed use lost overlay/warning'
|
|
3007
|
+
cp "$root/registry-good" "$record"
|
|
3008
|
+
rgt restore "$rr" > "$root/output"
|
|
3009
|
+
expect_status 0 'clean removes stamp record' rgt clean --stamp "$version" --apply
|
|
3010
|
+
[ ! -e "$rf/stamps/$version" ] && [ ! -e "$rf/nuget/Cratis.Registry.$version.nupkg" ] || failed 'clean retained stamp record/artifacts'
|
|
3011
|
+
[ -e "$rf/nuget/Cratis.Legacy.$legacy.nupkg" ] || failed 'registry clean removed another stamp'
|
|
3012
|
+
self_test_registry_invalid
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
self_test_registry_invalid() {
|
|
3016
|
+
local fixture rf bad_record other=1.2.3-train.gabcdef0
|
|
3017
|
+
for fixture in clean-scoped restore-other restore-no-node restore-own finish list; do
|
|
3018
|
+
rf=$root/registry-invalid-$fixture
|
|
3019
|
+
bad_record=$rf/stamps/$other/record.json
|
|
3020
|
+
mkdir -p "$rf/nuget" "${bad_record%/*}"
|
|
3021
|
+
printf '{broken\n' > "$bad_record"
|
|
3022
|
+
: > "$rf/nuget/Cratis.Test.$version.nupkg"
|
|
3023
|
+
rgt use-nuget "$rr" "$version" > "$root/output"
|
|
3024
|
+
if [ "$fixture" = restore-own ]; then
|
|
3025
|
+
bad_record=$rf/stamps/$version/record.json
|
|
3026
|
+
printf '{broken\n' > "$bad_record"
|
|
3027
|
+
fi
|
|
3028
|
+
cp "$bad_record" "$root/invalid-before"
|
|
3029
|
+
case "$fixture" in
|
|
3030
|
+
clean-scoped)
|
|
3031
|
+
rgt restore "$rr" > "$root/output"
|
|
3032
|
+
expect_status 0 'registry invalid other record clean scoped' rgt clean --stamp "$version" --apply
|
|
3033
|
+
[ ! -e "$rf/stamps/$version" ] && [ ! -e "$rf/nuget/Cratis.Test.$version.nupkg" ] || failed 'invalid other record blocked scoped clean'
|
|
3034
|
+
expect_status 2 'registry invalid record clean all' rgt clean --all --apply
|
|
3035
|
+
expect_status 2 'registry invalid record clean default' rgt clean --apply
|
|
3036
|
+
;;
|
|
3037
|
+
restore-other)
|
|
3038
|
+
expect_status 0 'registry invalid other record restore' rgt restore "$rr"
|
|
3039
|
+
;;
|
|
3040
|
+
restore-no-node)
|
|
3041
|
+
if PATH=/usr/bin:/bin:/usr/sbin:/sbin command -v node > /dev/null 2>&1; then
|
|
3042
|
+
printf 'SKIP: restore without node needs a system PATH without node\n' >&2; skipped=$((skipped + 1))
|
|
3043
|
+
rgt restore "$rr" > "$root/output"
|
|
3044
|
+
else
|
|
3045
|
+
expect_status 0 'registry restore without node restores the overlay' env -i HOME="$home" PATH=/usr/bin:/bin:/usr/sbin:/sbin TMPDIR="$TMP" CRATIS_LOCAL_FEED="$rf" NUGET_PACKAGES="$root/registry-cache" /bin/bash "$SELF" restore "$rr"
|
|
3046
|
+
{ [ ! -e "$rs/env" ] && grep -Fq 'WARNING: node not found' "$root/stderr"; } || failed 'restore without node kept overlay or missed warning'
|
|
3047
|
+
fi
|
|
3048
|
+
;;
|
|
3049
|
+
restore-own)
|
|
3050
|
+
expect_status 1 'registry invalid own record restore' rgt restore "$rr"
|
|
3051
|
+
{ grep -Fq 'WARNING:' "$root/stderr" && grep -Fq "$bad_record" "$root/stderr"; } || failed 'restore missing invalid record warning'
|
|
3052
|
+
;;
|
|
3053
|
+
finish)
|
|
3054
|
+
expect_status 1 'registry finish skips invalid' rgt finish --task "$rt" --apply
|
|
3055
|
+
record=$rf/stamps/$version/record.json rg_assert finished "$version" "$rr" "$rs" || failed 'finish skipped valid record'
|
|
3056
|
+
{ grep -Fq 'WARNING:' "$root/stderr" && grep -Fq "$bad_record" "$root/stderr"; } || failed 'finish missing invalid record warning'
|
|
3057
|
+
rgt restore "$rr" > "$root/output"
|
|
3058
|
+
;;
|
|
3059
|
+
list)
|
|
3060
|
+
expect_status 1 'list invalid record' rgt list --json
|
|
3061
|
+
node - "$root/output" "$version" "$other" <<'NODE' || failed 'list missing valid/invalid rows'
|
|
3062
|
+
const fs = require('node:fs'); const [file, valid, invalid] = process.argv.slice(2);
|
|
3063
|
+
const rows = JSON.parse(fs.readFileSync(file, 'utf8')).stamps;
|
|
3064
|
+
if (rows.length !== 2 || rows.find(r => r.stamp === valid)?.recorded !== true ||
|
|
3065
|
+
rows.find(r => r.stamp === invalid)?.recorded !== 'invalid' || typeof rows.find(r => r.stamp === invalid)?.error !== 'string') process.exit(1);
|
|
3066
|
+
NODE
|
|
3067
|
+
expect_status 1 'list invalid record text' rgt list
|
|
3068
|
+
{ grep -Fq invalid "$root/output" && grep -Fq "$version" "$root/output"; } || failed 'text list missing valid/invalid rows'
|
|
3069
|
+
rgt restore "$rr" > "$root/output"
|
|
3070
|
+
;;
|
|
3071
|
+
esac
|
|
3072
|
+
[ ! -e "$rs/env" ] && [ ! -e "$rs/Directory.Packages.props" ] || failed 'invalid record prevented overlay restore'
|
|
3073
|
+
cmp -s "$bad_record" "$root/invalid-before" || failed 'invalid record rewritten'
|
|
3074
|
+
done
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
self_test_private_cache() {
|
|
3078
|
+
local pc_repo=$root/'private cache repo' pc_feed=$root/private-feed pc_shared=$root/private-shared
|
|
3079
|
+
local pc_cache pc_session pc_opt=0 other=1.2.5-train.g1234567
|
|
3080
|
+
mkdir -p "$pc_repo" "$pc_feed/nuget"
|
|
3081
|
+
git -C "$pc_repo" init -q
|
|
3082
|
+
cp "$repo/Directory.Packages.props" "$pc_repo/Directory.Packages.props"
|
|
3083
|
+
git -C "$pc_repo" add Directory.Packages.props
|
|
3084
|
+
git -C "$pc_repo" -c user.email=test@example.invalid -c user.name='Private cache fixture' commit -qm initial
|
|
3085
|
+
pc_cache=$(git -C "$pc_repo" rev-parse --absolute-git-dir)/cratis-local-nuget
|
|
3086
|
+
pc_session=$(git -C "$pc_repo" rev-parse --absolute-git-dir)/cratis-local
|
|
3087
|
+
pct() { env -i HOME="$home" PATH="$system_path" TMPDIR="$TMP" CRATIS_LOCAL_FEED="$pc_feed" NUGET_PACKAGES="$pc_shared" CRATIS_LOCAL_PRIVATE_CACHE="$pc_opt" /bin/bash "$SELF" "$@"; }
|
|
3088
|
+
: > "$pc_feed/nuget/Cratis.Test.$version.nupkg"
|
|
3089
|
+
expect_status 0 'private-cache default unchanged' pct use-nuget "$pc_repo" "$version"
|
|
3090
|
+
[ ! -e "$pc_cache" ] && [ ! -e "$pc_session/nuget-cache" ] || failed 'private-cache default created a cache'
|
|
3091
|
+
has 'Optional per-worktree cache' "$root/output" || failed 'private-cache default hint changed'
|
|
3092
|
+
pct restore "$pc_repo" > "$root/output"
|
|
3093
|
+
expect_status 0 'private-cache dry-run' pct --dry-run use-nuget "$pc_repo" "$version" --private-cache
|
|
3094
|
+
[ ! -e "$pc_cache" ] && [ ! -e "$pc_session/nuget-cache" ] || failed 'private-cache dry-run wrote a cache'
|
|
3095
|
+
expect_status 0 'private-cache env export' pct use-nuget "$pc_repo" "$version" --private-cache
|
|
3096
|
+
env -i PATH=/usr/bin:/bin /bin/bash -c "source \"\$1\"; [ \"\$NUGET_PACKAGES\" = \"\$2\" ]" _ "$pc_session/env" "$pc_cache" || failed 'private-cache env does not export gitdir cache'
|
|
3097
|
+
if has 'Optional per-worktree cache' "$root/output"; then failed 'private-cache still prints manual hint'; fi
|
|
3098
|
+
pc_marker() { [ "$(cat "$pc_cache/.cratis-local-cache")" = "$pc_repo" ] && [ "$(cat "$pc_session/nuget-cache")" = "$pc_cache" ]; }
|
|
3099
|
+
expect_status 0 'private-cache marker' pc_marker
|
|
3100
|
+
expect_status 2 'private-cache live kept' pct clean "$pc_repo" --private-cache --apply
|
|
3101
|
+
[ -d "$pc_cache" ] || failed 'private-cache live cache removed'
|
|
3102
|
+
pct restore "$pc_repo" > "$root/output"
|
|
3103
|
+
[ -f "$pc_session/nuget-cache" ] && [ -d "$pc_cache" ] || failed 'private-cache restore removed cache'
|
|
3104
|
+
mkdir -p "$pc_cache/cratis.test/$version" "$pc_cache/cratis.test/$other" "$pc_cache/cratis.test/1.2.3" "$pc_cache/cratis.test/1.2.3-local.keep"
|
|
3105
|
+
mkdir -p "$pc_shared/cratis.test/$version" "$pc_shared/cratis.test/$other" "$pc_shared/cratis.test/1.2.3"
|
|
3106
|
+
expect_status 0 'private-cache clean scoped' pct clean --stamp "$version" --apply
|
|
3107
|
+
[ ! -e "$pc_cache/cratis.test/$version" ] && [ -d "$pc_cache/cratis.test/$other" ] && [ -d "$pc_cache/cratis.test/1.2.3" ] && [ -d "$pc_cache/cratis.test/1.2.3-local.keep" ] || failed 'private-cache stamp scope violated'
|
|
3108
|
+
[ ! -e "$pc_shared/cratis.test/$version" ] || failed 'private-cache shared local version retained'
|
|
3109
|
+
expect_status 0 'private-cache shared cache only local' pct clean --all --apply
|
|
3110
|
+
[ ! -e "$pc_shared/cratis.test/$other" ] && [ -d "$pc_shared/cratis.test/1.2.3" ] || failed 'private-cache shared non-local folder removed'
|
|
3111
|
+
rm -- "$pc_cache/.cratis-local-cache"
|
|
3112
|
+
mkdir -p "$pc_shared/cratis.test/$version"
|
|
3113
|
+
expect_status 2 'private-cache unmarked refused' pct clean --stamp "$version" --apply
|
|
3114
|
+
[ -d "$pc_shared/cratis.test/$version" ] || failed 'private-cache unmarked refusal mutated shared cache'
|
|
3115
|
+
printf '%s\n' "$root/other-repo" > "$pc_cache/.cratis-local-cache"
|
|
3116
|
+
expect_status 2 'private-cache mismatched marker refused' pct clean --all --apply
|
|
3117
|
+
printf '%s\n' "$pc_repo" > "$pc_cache/.cratis-local-cache"
|
|
3118
|
+
mv "$pc_cache" "$pc_cache.saved"; ln -s "$pc_cache.saved" "$pc_cache"
|
|
3119
|
+
expect_status 2 'private-cache symlink refused' pct clean --apply
|
|
3120
|
+
rm -- "$pc_cache"; mv "$pc_cache.saved" "$pc_cache"
|
|
3121
|
+
printf '%s\n' "$pc_shared" > "$pc_session/nuget-cache"
|
|
3122
|
+
expect_status 2 'private-cache outside gitdir refused' pct clean --apply
|
|
3123
|
+
printf '%s\n' "$pc_cache" > "$pc_session/nuget-cache"
|
|
3124
|
+
printf '<Project><!-- %s --></Project>\n' "$other" > "$pc_repo/Directory.Packages.props"
|
|
3125
|
+
expect_status 2 'private-cache restored manifest live kept' pct clean "$pc_repo" --private-cache --stamp "$version" --apply
|
|
3126
|
+
cp "$repo/Directory.Packages.props" "$pc_repo/Directory.Packages.props"
|
|
3127
|
+
expect_status 0 'private-cache whole dry-run' pct clean "$pc_repo" --private-cache
|
|
3128
|
+
[ -d "$pc_cache" ] && [ -f "$pc_session/nuget-cache" ] || failed 'private-cache whole dry-run mutated cache'
|
|
3129
|
+
expect_status 0 'private-cache whole removal' pct clean "$pc_repo" --private-cache --apply
|
|
3130
|
+
[ ! -e "$pc_cache" ] && [ ! -e "$pc_session/nuget-cache" ] && [ -d "$pc_shared/cratis.test/1.2.3" ] || failed 'private-cache whole removal scope violated'
|
|
3131
|
+
: > "$pc_feed/nuget/Cratis.Test.$version.nupkg"
|
|
3132
|
+
pc_opt=1
|
|
3133
|
+
expect_status 0 'private-cache environment opt-in' pct use-nuget "$pc_repo" "$version"
|
|
3134
|
+
pc_marker || failed 'private-cache environment opt-in missing marker'
|
|
3135
|
+
pct restore "$pc_repo" > "$root/output"
|
|
3136
|
+
pct clean "$pc_repo" --private-cache --apply > "$root/output"
|
|
3137
|
+
}
|
|
3138
|
+
|
|
2226
3139
|
self_test_base_version() {
|
|
2227
3140
|
local base mode actual status
|
|
2228
3141
|
for mode in local train; do
|
|
@@ -2264,7 +3177,7 @@ self_test_live_clean() {
|
|
|
2264
3177
|
git -C "$lr" add Directory.Packages.props
|
|
2265
3178
|
git -C "$lr" -c user.email=test@example.invalid -c user.name='Live clean fixture' commit -qm initial
|
|
2266
3179
|
ls=$(git -C "$lr" rev-parse --absolute-git-dir)/cratis-local
|
|
2267
|
-
lct() { env -i HOME="$home" PATH
|
|
3180
|
+
lct() { env -i HOME="$home" PATH="$system_path" TMPDIR="$TMP" CRATIS_LOCAL_FEED="$lf" NUGET_PACKAGES="$root/live-cache" /bin/bash "$SELF" "$@"; }
|
|
2268
3181
|
: > "$lf/nuget/Cratis.Live.$lv.nupkg"
|
|
2269
3182
|
: > "$lf/nuget/Cratis.Unused.$uv.nupkg"
|
|
2270
3183
|
mkdir -p "$root/live-cache/cratis.live/$lv" "$root/live-cache/cratis.live/5.0.0"
|
|
@@ -2604,7 +3517,7 @@ self_test_matrix() {
|
|
|
2604
3517
|
self_test_composition
|
|
2605
3518
|
}
|
|
2606
3519
|
|
|
2607
|
-
#
|
|
3520
|
+
# Registry paths allow system/configured symlink parents of the feed.
|
|
2608
3521
|
self_test_symlink_feed() {
|
|
2609
3522
|
local physical=$root/feed-physical-parent alias=$root/feed-symlink-parent
|
|
2610
3523
|
local sr=$root/feed-symlink-repo sv=1.2.3-local.202610240544.gabcdef0
|
|
@@ -2615,7 +3528,7 @@ self_test_symlink_feed() {
|
|
|
2615
3528
|
printf '<Project><ItemGroup><PackageVersion Include="Cratis.Symlink" Version="1.2.3"/></ItemGroup></Project>\n' > "$sr/Directory.Packages.props"
|
|
2616
3529
|
git -C "$sr" add .gitignore Directory.Packages.props
|
|
2617
3530
|
git -C "$sr" -c user.email=test@example.invalid -c user.name='Symlink feed fixture' commit -qm initial
|
|
2618
|
-
slf() { env -i HOME="$home" PATH
|
|
3531
|
+
slf() { env -i HOME="$home" PATH="$system_path" TMPDIR="$TMP" CRATIS_LOCAL_FEED="$alias/missing/feed" CRATIS_LOCAL_SESSION_DIR="$sr/.ai-work/local-packages" /bin/bash "$SELF" "$@"; }
|
|
2619
3532
|
expect_status 0 'symlink feed resolution does not create a missing feed' slf check "$sr"
|
|
2620
3533
|
[ ! -e "$physical/missing" ] || failed 'symlink feed resolution created directories'
|
|
2621
3534
|
mkdir -p "$physical/missing/feed/nuget"
|
|
@@ -2767,6 +3680,16 @@ JSON
|
|
|
2767
3680
|
expect_status 1 'composition malformed assets' cst verify-assets "$cr" --composition "$cm"
|
|
2768
3681
|
cp "$root/composition-assets.json" "$cr/obj/project.assets.json"
|
|
2769
3682
|
expect_status 0 'composition activation with transitive pinning' cst use-nuget "$cr" --composition "$cm" --pin-transitive
|
|
3683
|
+
composition_registry() {
|
|
3684
|
+
node - "$cf" "$cr" "$a" "$b" "$c" <<'NODE'
|
|
3685
|
+
const fs = require('node:fs'), path = require('node:path'); const [feed,repo,...stamps] = process.argv.slice(2);
|
|
3686
|
+
for (const stamp of stamps) {
|
|
3687
|
+
const r = JSON.parse(fs.readFileSync(path.join(feed,'stamps',stamp,'record.json')));
|
|
3688
|
+
if (r.schema !== 1 || r.consumers.length !== 1 || r.consumers[0].worktree !== repo || r.consumers[0].ecosystem !== 'nuget') process.exit(1);
|
|
3689
|
+
}
|
|
3690
|
+
NODE
|
|
3691
|
+
}
|
|
3692
|
+
expect_status 0 'registry composition consumer' composition_registry
|
|
2770
3693
|
output=$cr/.ai-work/local-packages/composition
|
|
2771
3694
|
grep -Fq 'CentralPackageTransitivePinningEnabled>true' "$output/Directory.Packages.props" || failed 'composition pin property'
|
|
2772
3695
|
self_test_nuget_overlay "$output" Cratis.Stage "$c"
|
|
@@ -2802,6 +3725,23 @@ JSON
|
|
|
2802
3725
|
expect_status 0 'composition repeated restore' cst restore "$cr"
|
|
2803
3726
|
[ ! -e "$output" ] || failed 'composition overlay survives restore'
|
|
2804
3727
|
cmp -s "$cr/Directory.Packages.props" "$root/composition-original.props" || failed 'composition consumer props mutated'
|
|
3728
|
+
# Whole-cache cleanup also cleans feed artifacts. Keep this fixture's feed
|
|
3729
|
+
# separate so the recovery fixtures below retain their declared packages.
|
|
3730
|
+
local composition_feed=$cf composition_cache
|
|
3731
|
+
cf=$root/private-composition-feed
|
|
3732
|
+
mkdir -p "$cf/nuget"
|
|
3733
|
+
cp "$composition_feed/nuget/"*.nupkg "$cf/nuget/"
|
|
3734
|
+
expect_status 0 'private-cache composition' cst use-nuget "$cr" --composition "$cm" --private-cache
|
|
3735
|
+
composition_cache=$(git -C "$cr" rev-parse --absolute-git-dir)/cratis-local-nuget
|
|
3736
|
+
env -i PATH=/usr/bin:/bin /bin/bash -c "source \"\$1\"; [ \"\$NUGET_PACKAGES\" = \"\$2\" ]" _ "$output/env" "$composition_cache" || failed 'private-cache composition env export'
|
|
3737
|
+
[ "$(cat "$cr/.ai-work/local-packages/nuget-cache")" = "$composition_cache" ] && [ "$(cat "$composition_cache/.cratis-local-cache")" = "$cr" ] || failed 'private-cache composition marker'
|
|
3738
|
+
expect_status 2 'private-cache composition live kept' cst clean "$cr" --private-cache --apply
|
|
3739
|
+
expect_status 0 'private-cache composition restore' cst restore "$cr"
|
|
3740
|
+
[ -d "$composition_cache" ] || failed 'private-cache composition restore removed cache'
|
|
3741
|
+
expect_status 0 'private-cache composition removal' cst clean "$cr" --private-cache --apply
|
|
3742
|
+
[ ! -e "$composition_cache" ] && [ ! -e "$cr/.ai-work/local-packages/nuget-cache" ] || failed 'private-cache composition removal left cache registration'
|
|
3743
|
+
cf=$composition_feed
|
|
3744
|
+
[ -f "$cf/nuget/Cratis.Chronicle.InProcess.$a.nupkg" ] && [ -f "$cf/nuget/Cratis.Screenplay.$b.nupkg" ] && [ -f "$cf/nuget/Cratis.Stage.$c.nupkg" ] || failed 'private-cache composition cleanup removed recovery feed packages'
|
|
2805
3745
|
# Every live mutation boundary is exercised both as an ordinary failure and
|
|
2806
3746
|
# an uncatchable parent/child interruption, then recovered by the next call.
|
|
2807
3747
|
for kind in fail interrupt; do
|
|
@@ -2859,7 +3799,26 @@ JSON
|
|
|
2859
3799
|
passed=$((passed + 1))
|
|
2860
3800
|
}
|
|
2861
3801
|
|
|
3802
|
+
TASK=''
|
|
3803
|
+
case "$MODE" in
|
|
3804
|
+
pack-nuget|pack-npm|use-nuget|use-npm|finish)
|
|
3805
|
+
TASK=${CRATIS_LOCAL_TASK:-}
|
|
3806
|
+
ARGS=()
|
|
3807
|
+
while [ $# -gt 0 ]; do
|
|
3808
|
+
if [ "$1" = --task ]; then
|
|
3809
|
+
[ $# -ge 2 ] || die '--task requires an ID'
|
|
3810
|
+
TASK=$2; [ -n "$TASK" ] || die 'invalid task ID'; shift 2
|
|
3811
|
+
else ARGS+=("$1"); shift; fi
|
|
3812
|
+
done
|
|
3813
|
+
set -- ${ARGS[@]+"${ARGS[@]}"}
|
|
3814
|
+
if [ -n "$TASK" ]; then [[ "$TASK" =~ ^[A-Za-z0-9][A-Za-z0-9._#:/-]{0,127}$ ]] || die 'invalid task ID'; fi ;;
|
|
3815
|
+
esac
|
|
2862
3816
|
case "$MODE" in
|
|
3817
|
+
list) registry list "$@" ;;
|
|
3818
|
+
finish)
|
|
3819
|
+
[ -n "$TASK" ] || die 'finish requires --task ID or CRATIS_LOCAL_TASK'
|
|
3820
|
+
[ $# -eq 0 ] || { [ $# -eq 1 ] && [ "$1" = --apply ]; } || die 'finish accepts --task ID and --apply'
|
|
3821
|
+
if [ "${1:-}" = --apply ] && [ "$DRY" -eq 0 ]; then registry_update finish --apply; else registry finish; fi ;;
|
|
2863
3822
|
stamp) stamp "$@" ;;
|
|
2864
3823
|
plan) plan_command "$@" ;;
|
|
2865
3824
|
closure) closure "$@" ;;
|