@releaseway/npm-actions-fixture 0.0.6-native.0 → 0.0.9-native.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repository": "releaseway/npm-actions-fixture",
3
- "version": "0.0.6-native.0",
4
- "tag": "native-v0.0.6-native.0",
3
+ "version": "0.0.9-native.0",
4
+ "tag": "native-v0.0.9-native.0",
5
5
  "targets": {
6
6
  "linux-x64-gnu": {
7
7
  "asset": "npm-actions-native-fixture_linux_x64.tar.gz",
@@ -531,7 +531,7 @@ var require_yauzl = __commonJS({
531
531
  var PassThrough = require("stream").PassThrough;
532
532
  var Writable = require("stream").Writable;
533
533
  var crc32 = typeof zlib.crc32 === "function" ? zlib.crc32 : require_crc32();
534
- exports2.open = open;
534
+ exports2.open = open2;
535
535
  exports2.fromFd = fromFd;
536
536
  exports2.fromBuffer = fromBuffer;
537
537
  exports2.fromRandomAccessReader = fromRandomAccessReader;
@@ -549,7 +549,7 @@ var require_yauzl = __commonJS({
549
549
  exports2.RandomAccessReader = RandomAccessReader;
550
550
  function openPromise(path, options2) {
551
551
  return new Promise((resolve2, reject) => {
552
- open(path, { ...options2, lazyEntries: true }, function(err, zipfile) {
552
+ open2(path, { ...options2, lazyEntries: true }, function(err, zipfile) {
553
553
  if (err) return reject(err);
554
554
  resolve2(zipfile);
555
555
  });
@@ -579,7 +579,7 @@ var require_yauzl = __commonJS({
579
579
  });
580
580
  });
581
581
  }
582
- function open(path, options2, callback) {
582
+ function open2(path, options2, callback) {
583
583
  if (typeof options2 === "function") {
584
584
  callback = options2;
585
585
  options2 = null;
@@ -1387,17 +1387,97 @@ __export(runtime_exports, {
1387
1387
  });
1388
1388
  module.exports = __toCommonJS(runtime_exports);
1389
1389
  var import_node_child_process2 = require("node:child_process");
1390
- var import_node_process3 = __toESM(require("node:process"));
1390
+ var import_node_process3 = __toESM(require("node:process"), 1);
1391
1391
 
1392
1392
  // src/native/launcher/cache.ts
1393
1393
  var import_node_crypto3 = require("node:crypto");
1394
- var import_promises2 = require("node:fs/promises");
1394
+ var import_node_fs8 = require("node:fs");
1395
+ var import_promises3 = require("node:fs/promises");
1395
1396
  var import_node_os = require("node:os");
1396
1397
  var import_node_path11 = require("node:path");
1397
- var import_node_process = __toESM(require("node:process"));
1398
- var import_promises3 = require("node:timers/promises");
1398
+ var import_node_process = __toESM(require("node:process"), 1);
1399
+
1400
+ // src/native/limits.ts
1401
+ function nativeLimits(env = process.env) {
1402
+ const read = (suffix, fallback) => {
1403
+ const name = `RELEASEWAY_NATIVE_${suffix}`;
1404
+ const raw = env[name];
1405
+ if (raw === void 0 || raw === "") return fallback;
1406
+ const value = Number(raw);
1407
+ if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value <= 0) {
1408
+ throw new Error(`${name} must be a positive safe integer`);
1409
+ }
1410
+ return value;
1411
+ };
1412
+ return {
1413
+ apiTimeoutMs: read("API_TIMEOUT_MS", 3e4),
1414
+ downloadTimeoutMs: read("DOWNLOAD_TIMEOUT_MS", 24e4),
1415
+ extractTimeoutMs: read("EXTRACT_TIMEOUT_MS", 12e4),
1416
+ maxArchiveBytes: read("MAX_ARCHIVE_BYTES", 1024 ** 3),
1417
+ maxExecutableBytes: read("MAX_EXECUTABLE_BYTES", 1024 ** 3),
1418
+ maxExpandedBytes: read("MAX_EXPANDED_BYTES", 4 * 1024 ** 3)
1419
+ };
1420
+ }
1421
+ async function withDeadline(label, timeoutMs, operation) {
1422
+ const controller = new AbortController();
1423
+ let timer;
1424
+ const expired = new Promise((_2, reject) => {
1425
+ const deadline = performance.now() + timeoutMs;
1426
+ const expire = () => {
1427
+ const remaining = deadline - performance.now();
1428
+ if (remaining > 0) {
1429
+ timer = setTimeout(expire, Math.min(Math.ceil(remaining), 2 ** 31 - 1));
1430
+ return;
1431
+ }
1432
+ const error = new Error(`${label} exceeded ${timeoutMs}ms preparation budget`);
1433
+ controller.abort(error);
1434
+ reject(error);
1435
+ };
1436
+ expire();
1437
+ });
1438
+ try {
1439
+ return await Promise.race([operation(controller.signal), expired]);
1440
+ } finally {
1441
+ clearTimeout(timer);
1442
+ }
1443
+ }
1444
+ async function consumeBoundedResponse(response, maxBytes, signal, consume) {
1445
+ const declared = Number(response.headers.get("content-length"));
1446
+ if (Number.isFinite(declared) && declared > maxBytes) {
1447
+ await response.body?.cancel();
1448
+ throw new Error(`Download exceeds ${maxBytes} byte limit`);
1449
+ }
1450
+ if (!response.body) return;
1451
+ const reader = response.body.getReader();
1452
+ const abort = () => {
1453
+ void reader.cancel(signal.reason).catch(() => {
1454
+ });
1455
+ };
1456
+ signal.addEventListener("abort", abort, { once: true });
1457
+ let total = 0;
1458
+ try {
1459
+ while (true) {
1460
+ signal.throwIfAborted();
1461
+ const { done, value } = await reader.read();
1462
+ signal.throwIfAborted();
1463
+ if (done) break;
1464
+ total += value.byteLength;
1465
+ if (total > maxBytes) throw new Error(`Download exceeds ${maxBytes} byte limit`);
1466
+ await consume(value);
1467
+ }
1468
+ } finally {
1469
+ signal.removeEventListener("abort", abort);
1470
+ await reader.cancel().catch(() => {
1471
+ });
1472
+ reader.releaseLock();
1473
+ }
1474
+ }
1475
+
1476
+ // src/native/launcher/cache.ts
1477
+ var import_promises4 = require("node:timers/promises");
1399
1478
 
1400
1479
  // src/native/launcher/archive.ts
1480
+ var import_node_fs7 = require("node:fs");
1401
1481
  var import_node_path10 = require("node:path");
1402
1482
 
1403
1483
  // node_modules/tar/dist/esm/index.min.js
@@ -4370,7 +4450,7 @@ var To = (s3) => {
4370
4450
  };
4371
4451
 
4372
4452
  // src/native/launcher/archive.ts
4373
- var import_yauzl = __toESM(require_yauzl());
4453
+ var import_yauzl = __toESM(require_yauzl(), 1);
4374
4454
  function normalizeArchivePath(value) {
4375
4455
  if (value.length === 0 || value.includes("\0") || value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/.test(value)) {
4376
4456
  throw new Error(`Unsafe archive entry path: ${value}`);
@@ -4385,44 +4465,73 @@ function normalizeArchivePath(value) {
4385
4465
  function isTarRegularFile(type) {
4386
4466
  return type === "File" || type === "OldFile" || type === "ContiguousFile";
4387
4467
  }
4388
- async function extractTarGzExecutable(archivePath, executablePath) {
4468
+ async function extractTarGzExecutable(archivePath, executablePath, limits = nativeLimits()) {
4389
4469
  const wanted = normalizeArchivePath(executablePath);
4390
4470
  const seen = /* @__PURE__ */ new Set();
4391
4471
  const chunks = [];
4392
4472
  let matches = 0;
4393
4473
  let scanError;
4394
- await Ct({
4395
- file: archivePath,
4396
- strict: true,
4397
- onentry(entry) {
4398
- if (scanError) {
4399
- entry.resume();
4400
- return;
4401
- }
4402
- try {
4403
- const normalized = normalizeArchivePath(entry.path.replace(/\/$/, ""));
4404
- if (seen.has(normalized)) {
4405
- throw new Error(`Duplicate archive entry path: ${normalized}`);
4406
- }
4407
- seen.add(normalized);
4408
- if (entry.type === "Directory") {
4474
+ let expanded = 0;
4475
+ let executableBytes = 0;
4476
+ await withDeadline("Native tar extraction", limits.extractTimeoutMs, (signal) => new Promise((resolve2, reject) => {
4477
+ const input = (0, import_node_fs7.createReadStream)(archivePath);
4478
+ const parser = Ct({
4479
+ strict: true,
4480
+ onentry(entry) {
4481
+ if (scanError) {
4482
+ entry.resume();
4409
4483
  return;
4410
4484
  }
4411
- if (!isTarRegularFile(entry.type)) {
4412
- throw new Error(
4413
- `Unsupported non-regular tar entry ${entry.type}: ${entry.path}`
4414
- );
4415
- }
4416
- if (normalized === wanted) {
4417
- matches += 1;
4418
- entry.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
4485
+ try {
4486
+ expanded += entry.size;
4487
+ if (expanded > limits.maxExpandedBytes) throw new Error("Native archive exceeds expanded byte limit");
4488
+ const normalized = normalizeArchivePath(entry.path.replace(/\/$/, ""));
4489
+ if (seen.has(normalized)) {
4490
+ throw new Error(`Duplicate archive entry path: ${normalized}`);
4491
+ }
4492
+ seen.add(normalized);
4493
+ if (entry.type === "Directory") {
4494
+ return;
4495
+ }
4496
+ if (!isTarRegularFile(entry.type)) {
4497
+ throw new Error(
4498
+ `Unsupported non-regular tar entry ${entry.type}: ${entry.path}`
4499
+ );
4500
+ }
4501
+ if (normalized === wanted) {
4502
+ if (entry.size > limits.maxExecutableBytes) throw new Error("Native executable exceeds byte limit");
4503
+ matches += 1;
4504
+ entry.on("data", (chunk) => {
4505
+ executableBytes += chunk.length;
4506
+ if (executableBytes > limits.maxExecutableBytes) {
4507
+ parser.abort(new Error("Native executable exceeds byte limit"));
4508
+ return;
4509
+ }
4510
+ chunks.push(Buffer.from(chunk));
4511
+ });
4512
+ }
4513
+ } catch (error) {
4514
+ scanError = error instanceof Error ? error : new Error(String(error));
4515
+ entry.resume();
4516
+ parser.abort(scanError);
4419
4517
  }
4420
- } catch (error) {
4421
- scanError = error instanceof Error ? error : new Error(String(error));
4422
- entry.resume();
4423
4518
  }
4424
- }
4425
- });
4519
+ });
4520
+ const abort = () => parser.abort(signal.reason);
4521
+ signal.addEventListener("abort", abort, { once: true });
4522
+ const finish = (error) => {
4523
+ signal.removeEventListener("abort", abort);
4524
+ input.destroy();
4525
+ if (error) reject(error);
4526
+ else resolve2();
4527
+ };
4528
+ input.on("error", (error) => {
4529
+ parser.abort(error);
4530
+ });
4531
+ parser.on("error", finish);
4532
+ parser.on("end", () => finish());
4533
+ input.pipe(parser);
4534
+ }));
4426
4535
  if (scanError) {
4427
4536
  throw scanError;
4428
4537
  }
@@ -4464,90 +4573,117 @@ function openZip(path) {
4464
4573
  });
4465
4574
  });
4466
4575
  }
4467
- function readZipEntry(zip, entry) {
4576
+ function readZipEntry(zip, entry, maxBytes, signal) {
4468
4577
  return new Promise((resolve2, reject) => {
4469
4578
  zip.openReadStream(entry, (error, stream) => {
4470
4579
  if (error || !stream) {
4471
4580
  reject(error ?? new Error("Failed to read zip entry"));
4472
4581
  return;
4473
4582
  }
4583
+ if (signal.aborted) {
4584
+ stream.destroy();
4585
+ reject(signal.reason);
4586
+ return;
4587
+ }
4474
4588
  const chunks = [];
4475
- stream.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
4589
+ let total = 0;
4590
+ const abort = () => stream.destroy(signal.reason);
4591
+ signal.addEventListener("abort", abort, { once: true });
4592
+ stream.on("close", () => signal.removeEventListener("abort", abort));
4593
+ stream.on("data", (chunk) => {
4594
+ total += chunk.length;
4595
+ if (total > maxBytes) stream.destroy(new Error("Native executable exceeds byte limit"));
4596
+ else chunks.push(Buffer.from(chunk));
4597
+ });
4476
4598
  stream.on("error", reject);
4477
4599
  stream.on("end", () => resolve2(Buffer.concat(chunks)));
4478
4600
  });
4479
4601
  });
4480
4602
  }
4481
- async function extractZipExecutable(archivePath, executablePath) {
4482
- const wanted = normalizeArchivePath(executablePath);
4483
- const zip = await openZip(archivePath);
4484
- const seen = /* @__PURE__ */ new Set();
4485
- let match;
4486
- let matches = 0;
4487
- return new Promise((resolve2, reject) => {
4488
- let settled = false;
4489
- const fail = (error) => {
4490
- if (settled) return;
4491
- settled = true;
4492
- try {
4493
- zip.close();
4494
- } catch {
4495
- }
4496
- reject(error);
4497
- };
4498
- zip.on("error", fail);
4499
- zip.on("entry", async (entry) => {
4500
- try {
4501
- const raw = entry.fileName.replace(/\/$/, "");
4502
- const normalized = normalizeArchivePath(raw);
4503
- if (seen.has(normalized)) {
4504
- throw new Error(`Duplicate archive entry path: ${normalized}`);
4505
- }
4506
- seen.add(normalized);
4507
- assertZipEntryType(entry, normalized);
4508
- const directory = entry.fileName.endsWith("/") || ((zipUnixMode(entry) ?? 0) & 61440) === 16384;
4509
- if (normalized === wanted) {
4510
- if (directory) {
4511
- throw new Error(
4512
- `Configured native executable is not a regular zip file: ${wanted}`
4513
- );
4514
- }
4515
- matches += 1;
4516
- match = await readZipEntry(zip, entry);
4603
+ async function extractZipExecutable(archivePath, executablePath, limits = nativeLimits()) {
4604
+ return withDeadline("Native zip extraction", limits.extractTimeoutMs, async (signal) => {
4605
+ const wanted = normalizeArchivePath(executablePath);
4606
+ const zip = await openZip(archivePath);
4607
+ const seen = /* @__PURE__ */ new Set();
4608
+ let match;
4609
+ let matches = 0;
4610
+ let expanded = 0;
4611
+ return new Promise((resolve2, reject) => {
4612
+ let settled = false;
4613
+ const fail = (error) => {
4614
+ if (settled) return;
4615
+ settled = true;
4616
+ try {
4617
+ zip.close();
4618
+ } catch {
4517
4619
  }
4518
- zip.readEntry();
4519
- } catch (error) {
4520
- fail(error);
4521
- }
4522
- });
4523
- zip.on("end", () => {
4524
- if (settled) return;
4525
- settled = true;
4526
- if (matches !== 1 || !match) {
4527
- reject(
4528
- new Error(
4529
- `Native executable ${wanted} must occur exactly once in zip; found ${matches}`
4530
- )
4531
- );
4620
+ reject(error);
4621
+ };
4622
+ zip.on("error", fail);
4623
+ const abort = () => fail(signal.reason);
4624
+ signal.addEventListener("abort", abort, { once: true });
4625
+ zip.on("close", () => signal.removeEventListener("abort", abort));
4626
+ if (signal.aborted) {
4627
+ fail(signal.reason);
4532
4628
  return;
4533
4629
  }
4534
- resolve2(match);
4630
+ zip.on("entry", async (entry) => {
4631
+ try {
4632
+ expanded += entry.uncompressedSize;
4633
+ if (expanded > limits.maxExpandedBytes) throw new Error("Native archive exceeds expanded byte limit");
4634
+ const raw = entry.fileName.replace(/\/$/, "");
4635
+ const normalized = normalizeArchivePath(raw);
4636
+ if (seen.has(normalized)) {
4637
+ throw new Error(`Duplicate archive entry path: ${normalized}`);
4638
+ }
4639
+ seen.add(normalized);
4640
+ assertZipEntryType(entry, normalized);
4641
+ const directory = entry.fileName.endsWith("/") || ((zipUnixMode(entry) ?? 0) & 61440) === 16384;
4642
+ if (normalized === wanted) {
4643
+ if (directory) {
4644
+ throw new Error(
4645
+ `Configured native executable is not a regular zip file: ${wanted}`
4646
+ );
4647
+ }
4648
+ matches += 1;
4649
+ if (entry.uncompressedSize > limits.maxExecutableBytes) throw new Error("Native executable exceeds byte limit");
4650
+ match = await readZipEntry(zip, entry, limits.maxExecutableBytes, signal);
4651
+ }
4652
+ zip.readEntry();
4653
+ } catch (error) {
4654
+ fail(error);
4655
+ }
4656
+ });
4657
+ zip.on("end", () => {
4658
+ if (settled) return;
4659
+ settled = true;
4660
+ if (matches !== 1 || !match) {
4661
+ reject(
4662
+ new Error(
4663
+ `Native executable ${wanted} must occur exactly once in zip; found ${matches}`
4664
+ )
4665
+ );
4666
+ return;
4667
+ }
4668
+ resolve2(match);
4669
+ });
4670
+ zip.readEntry();
4535
4671
  });
4536
- zip.readEntry();
4537
4672
  });
4538
4673
  }
4539
- async function extractExecutable(archivePath, assetName, executablePath) {
4674
+ async function extractExecutable(archivePath, assetName, executablePath, limits = nativeLimits()) {
4540
4675
  if (assetName.endsWith(".tar.gz")) {
4541
- return extractTarGzExecutable(archivePath, executablePath);
4676
+ return extractTarGzExecutable(archivePath, executablePath, limits);
4542
4677
  }
4543
4678
  if (assetName.endsWith(".zip")) {
4544
- return extractZipExecutable(archivePath, executablePath);
4679
+ return extractZipExecutable(archivePath, executablePath, limits);
4545
4680
  }
4546
4681
  throw new Error(`Unsupported native archive format: ${assetName}`);
4547
4682
  }
4548
4683
 
4549
4684
  // src/native/launcher/download.ts
4550
4685
  var import_node_crypto2 = require("node:crypto");
4686
+ var import_promises2 = require("node:fs/promises");
4551
4687
  function encodeRepository(repository) {
4552
4688
  const parts = repository.split("/");
4553
4689
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
@@ -4555,6 +4691,32 @@ function encodeRepository(repository) {
4555
4691
  }
4556
4692
  return parts.map(encodeURIComponent).join("/");
4557
4693
  }
4694
+ async function downloadReleaseAssetToFile(repository, tag, asset, destination, expectedSha256, env = process.env, fetchImpl = fetch) {
4695
+ const limits = nativeLimits(env);
4696
+ const file = await (0, import_promises2.open)(destination, "wx", 384);
4697
+ try {
4698
+ await withDeadline("Native asset download", limits.downloadTimeoutMs, async (signal) => {
4699
+ const response = await fetchImpl(releaseAssetUrl(repository, tag, asset), {
4700
+ signal,
4701
+ redirect: "follow",
4702
+ headers: { "User-Agent": "releaseway-npm-actions-native" }
4703
+ });
4704
+ if (!response.ok) {
4705
+ throw new Error(`Failed to download native GitHub Release asset ${asset}: HTTP ${response.status}`);
4706
+ }
4707
+ const hash = (0, import_node_crypto2.createHash)("sha256");
4708
+ await consumeBoundedResponse(response, limits.maxArchiveBytes, signal, async (chunk) => {
4709
+ hash.update(chunk);
4710
+ await file.writeFile(chunk);
4711
+ });
4712
+ if (hash.digest("hex") !== expectedSha256.toLowerCase()) {
4713
+ throw new Error("Native asset SHA-256 digest mismatch");
4714
+ }
4715
+ });
4716
+ } finally {
4717
+ await file.close();
4718
+ }
4719
+ }
4558
4720
  function releaseAssetUrl(repository, tag, asset) {
4559
4721
  return `https://github.com/${encodeRepository(repository)}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(asset)}`;
4560
4722
  }
@@ -4568,25 +4730,16 @@ function verifySha256(bytes, expectedHex) {
4568
4730
  throw new Error("Native asset SHA-256 digest mismatch");
4569
4731
  }
4570
4732
  }
4571
- async function downloadReleaseAsset(repository, tag, asset, fetchImpl = fetch) {
4572
- const response = await fetchImpl(releaseAssetUrl(repository, tag, asset), {
4573
- redirect: "follow",
4574
- headers: {
4575
- "User-Agent": "releaseway-npm-actions-native"
4576
- }
4577
- });
4578
- if (!response.ok) {
4579
- throw new Error(
4580
- `Failed to download native GitHub Release asset ${asset}: HTTP ${response.status}`
4581
- );
4582
- }
4583
- return Buffer.from(await response.arrayBuffer());
4584
- }
4585
4733
 
4586
4734
  // src/native/launcher/cache.ts
4587
4735
  function sha256(bytes) {
4588
4736
  return (0, import_node_crypto3.createHash)("sha256").update(bytes).digest("hex");
4589
4737
  }
4738
+ async function fileSha256(path) {
4739
+ const hash = (0, import_node_crypto3.createHash)("sha256");
4740
+ for await (const chunk of (0, import_node_fs8.createReadStream)(path)) hash.update(chunk);
4741
+ return hash.digest("hex");
4742
+ }
4590
4743
  function archiveFormat(asset) {
4591
4744
  if (asset.endsWith(".tar.gz")) {
4592
4745
  return "tar.gz";
@@ -4600,6 +4753,12 @@ function nativeCacheRoot(options2 = {}) {
4600
4753
  const env = options2.env ?? import_node_process.default.env;
4601
4754
  const platform = options2.platform ?? import_node_process.default.platform;
4602
4755
  const home = options2.home ?? (0, import_node_os.homedir)();
4756
+ const packageOverride = options2.cacheEnv && env[options2.cacheEnv]?.length ? env[options2.cacheEnv] : void 0;
4757
+ const genericOverride = env.RELEASEWAY_NATIVE_CACHE_DIR?.length ? env.RELEASEWAY_NATIVE_CACHE_DIR : void 0;
4758
+ const override = packageOverride ?? genericOverride;
4759
+ if (override) {
4760
+ return (0, import_node_path11.resolve)(override);
4761
+ }
4603
4762
  if (platform === "linux") {
4604
4763
  const base = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.length > 0 ? env.XDG_CACHE_HOME : (0, import_node_path11.join)(home, ".cache");
4605
4764
  return (0, import_node_path11.resolve)(
@@ -4654,14 +4813,13 @@ function executableCacheKey(executable) {
4654
4813
  async function validateArchiveCache(directory, target) {
4655
4814
  try {
4656
4815
  const metadata = JSON.parse(
4657
- await (0, import_promises2.readFile)((0, import_node_path11.join)(directory, "archive.json"), "utf8")
4816
+ await (0, import_promises3.readFile)((0, import_node_path11.join)(directory, "archive.json"), "utf8")
4658
4817
  );
4659
4818
  if (metadata.schema !== 2 || metadata.assetSha256 !== target.sha256 || metadata.archiveFormat !== archiveFormat(target.asset)) {
4660
4819
  return void 0;
4661
4820
  }
4662
4821
  const archive = (0, import_node_path11.join)(directory, "archive.bin");
4663
- const bytes = await (0, import_promises2.readFile)(archive);
4664
- if (sha256(bytes) !== target.sha256) {
4822
+ if (await fileSha256(archive) !== target.sha256) {
4665
4823
  return void 0;
4666
4824
  }
4667
4825
  return archive;
@@ -4672,15 +4830,14 @@ async function validateArchiveCache(directory, target) {
4672
4830
  async function validateExecutableCache(directory, target, executable) {
4673
4831
  try {
4674
4832
  const metadata = JSON.parse(
4675
- await (0, import_promises2.readFile)((0, import_node_path11.join)(directory, "metadata.json"), "utf8")
4833
+ await (0, import_promises3.readFile)((0, import_node_path11.join)(directory, "metadata.json"), "utf8")
4676
4834
  );
4677
4835
  const executableFile = cachedExecutableName(executable);
4678
4836
  if (metadata.schema !== 2 || metadata.assetSha256 !== target.sha256 || metadata.sourceExecutable !== executable || typeof metadata.executableSha256 !== "string" || !/^[0-9a-f]{64}$/.test(metadata.executableSha256) || metadata.executableFile !== executableFile) {
4679
4837
  return void 0;
4680
4838
  }
4681
4839
  const path = (0, import_node_path11.join)(directory, executableFile);
4682
- const bytes = await (0, import_promises2.readFile)(path);
4683
- if (sha256(bytes) !== metadata.executableSha256) {
4840
+ if (await fileSha256(path) !== metadata.executableSha256) {
4684
4841
  return void 0;
4685
4842
  }
4686
4843
  return path;
@@ -4699,10 +4856,19 @@ async function acquireCacheLock(lockDirectory, validate, label, options2) {
4699
4856
  return { kind: "cached", value: cached };
4700
4857
  }
4701
4858
  try {
4702
- await (0, import_promises2.mkdir)(lockDirectory);
4859
+ await (0, import_promises3.mkdir)(lockDirectory);
4860
+ const heartbeat = setInterval(() => {
4861
+ const now = /* @__PURE__ */ new Date();
4862
+ void (0, import_promises3.utimes)(lockDirectory, now, now).catch(() => {
4863
+ });
4864
+ }, Math.max(1, Math.min(1e3, Math.floor(staleMs / 3))));
4865
+ heartbeat.unref();
4703
4866
  return {
4704
4867
  kind: "owner",
4705
- release: () => (0, import_promises2.rm)(lockDirectory, { recursive: true, force: true })
4868
+ release: async () => {
4869
+ clearInterval(heartbeat);
4870
+ await (0, import_promises3.rm)(lockDirectory, { recursive: true, force: true });
4871
+ }
4706
4872
  };
4707
4873
  } catch (error) {
4708
4874
  if (!error || typeof error !== "object" || !("code" in error) || String(error.code) !== "EEXIST") {
@@ -4710,9 +4876,9 @@ async function acquireCacheLock(lockDirectory, validate, label, options2) {
4710
4876
  }
4711
4877
  }
4712
4878
  try {
4713
- const lockStat = await (0, import_promises2.stat)(lockDirectory);
4879
+ const lockStat = await (0, import_promises3.stat)(lockDirectory);
4714
4880
  if (Date.now() - lockStat.mtimeMs > staleMs) {
4715
- await (0, import_promises2.rm)(lockDirectory, { recursive: true, force: true });
4881
+ await (0, import_promises3.rm)(lockDirectory, { recursive: true, force: true });
4716
4882
  continue;
4717
4883
  }
4718
4884
  } catch (error) {
@@ -4724,25 +4890,25 @@ async function acquireCacheLock(lockDirectory, validate, label, options2) {
4724
4890
  if (Date.now() - started > timeoutMs) {
4725
4891
  throw new Error(`Timed out waiting for native cache lock for ${label}`);
4726
4892
  }
4727
- await (0, import_promises3.setTimeout)(pollMs);
4893
+ await (0, import_promises4.setTimeout)(pollMs);
4728
4894
  }
4729
4895
  }
4730
4896
  async function promoteDirectory(temp, finalDirectory, validate) {
4731
4897
  try {
4732
- await (0, import_promises2.rename)(temp, finalDirectory);
4898
+ await (0, import_promises3.rename)(temp, finalDirectory);
4733
4899
  return validate();
4734
4900
  } catch (error) {
4735
4901
  if (!error || typeof error !== "object" || !("code" in error) || !["EEXIST", "ENOTEMPTY", "EPERM"].includes(String(error.code))) {
4736
4902
  throw error;
4737
4903
  }
4738
- await (0, import_promises2.rm)(temp, { recursive: true, force: true });
4904
+ await (0, import_promises3.rm)(temp, { recursive: true, force: true });
4739
4905
  return validate();
4740
4906
  }
4741
4907
  }
4742
4908
  async function prepareNativeArchive(manifest, target, root, options2) {
4743
4909
  const directory = (0, import_node_path11.join)(root, target.sha256);
4744
4910
  const executables = (0, import_node_path11.join)(directory, "executables");
4745
- await (0, import_promises2.mkdir)(executables, { recursive: true });
4911
+ await (0, import_promises3.mkdir)(executables, { recursive: true });
4746
4912
  const validate = () => validateArchiveCache(directory, target);
4747
4913
  const existing = await validate();
4748
4914
  if (existing) {
@@ -4762,34 +4928,35 @@ async function prepareNativeArchive(manifest, target, root, options2) {
4762
4928
  if (afterLock) {
4763
4929
  return afterLock;
4764
4930
  }
4765
- await (0, import_promises2.rm)((0, import_node_path11.join)(directory, "archive.bin"), { force: true });
4766
- await (0, import_promises2.rm)((0, import_node_path11.join)(directory, "archive.json"), { force: true });
4767
- const temp = await (0, import_promises2.mkdtemp)(
4931
+ await (0, import_promises3.rm)((0, import_node_path11.join)(directory, "archive.bin"), { force: true });
4932
+ await (0, import_promises3.rm)((0, import_node_path11.join)(directory, "archive.json"), { force: true });
4933
+ const temp = await (0, import_promises3.mkdtemp)(
4768
4934
  (0, import_node_path11.join)(root, `.${target.sha256}.archive.tmp-`)
4769
4935
  );
4770
4936
  try {
4771
- const download = options2.download ?? ((repository, tag, asset) => downloadReleaseAsset(repository, tag, asset));
4772
- const archiveBytes = await download(
4773
- manifest.repository,
4774
- manifest.tag,
4775
- target.asset
4776
- );
4777
- verifySha256(archiveBytes, target.sha256);
4778
4937
  const tempArchive = (0, import_node_path11.join)(temp, "archive.bin");
4779
4938
  const tempMetadata = (0, import_node_path11.join)(temp, "archive.json");
4780
- await (0, import_promises2.writeFile)(tempArchive, archiveBytes, { mode: 384 });
4939
+ if (options2.download) {
4940
+ const limits = nativeLimits(options2.env);
4941
+ const archiveBytes = await withDeadline("Native asset download", limits.downloadTimeoutMs, () => options2.download(manifest.repository, manifest.tag, target.asset));
4942
+ if (archiveBytes.length > limits.maxArchiveBytes) throw new Error("Native archive exceeds byte limit");
4943
+ verifySha256(archiveBytes, target.sha256);
4944
+ await (0, import_promises3.writeFile)(tempArchive, archiveBytes, { mode: 384 });
4945
+ } else {
4946
+ await downloadReleaseAssetToFile(manifest.repository, manifest.tag, target.asset, tempArchive, target.sha256, options2.env);
4947
+ }
4781
4948
  const metadata = {
4782
4949
  schema: 2,
4783
4950
  assetSha256: target.sha256,
4784
4951
  archiveFormat: archiveFormat(target.asset)
4785
4952
  };
4786
- await (0, import_promises2.writeFile)(
4953
+ await (0, import_promises3.writeFile)(
4787
4954
  tempMetadata,
4788
4955
  JSON.stringify(metadata, null, 2) + "\n",
4789
4956
  { encoding: "utf8", mode: 384 }
4790
4957
  );
4791
- await (0, import_promises2.rename)(tempArchive, (0, import_node_path11.join)(directory, "archive.bin"));
4792
- await (0, import_promises2.rename)(tempMetadata, (0, import_node_path11.join)(directory, "archive.json"));
4958
+ await (0, import_promises3.rename)(tempArchive, (0, import_node_path11.join)(directory, "archive.bin"));
4959
+ await (0, import_promises3.rename)(tempMetadata, (0, import_node_path11.join)(directory, "archive.json"));
4793
4960
  const promoted = await validate();
4794
4961
  if (!promoted) {
4795
4962
  throw new Error(
@@ -4798,7 +4965,7 @@ async function prepareNativeArchive(manifest, target, root, options2) {
4798
4965
  }
4799
4966
  return promoted;
4800
4967
  } finally {
4801
- await (0, import_promises2.rm)(temp, { recursive: true, force: true });
4968
+ await (0, import_promises3.rm)(temp, { recursive: true, force: true });
4802
4969
  }
4803
4970
  } finally {
4804
4971
  await lock.release();
@@ -4828,19 +4995,20 @@ async function prepareExecutableFromArchive(archivePath, target, root, options2)
4828
4995
  if (afterLock) {
4829
4996
  return afterLock;
4830
4997
  }
4831
- await (0, import_promises2.rm)(finalDirectory, { recursive: true, force: true });
4832
- const temp = await (0, import_promises2.mkdtemp)((0, import_node_path11.join)(executables, `.${key}.tmp-`));
4998
+ await (0, import_promises3.rm)(finalDirectory, { recursive: true, force: true });
4999
+ const temp = await (0, import_promises3.mkdtemp)((0, import_node_path11.join)(executables, `.${key}.tmp-`));
4833
5000
  try {
4834
5001
  const executableBytes = await extractExecutable(
4835
5002
  archivePath,
4836
5003
  target.asset,
4837
- executable
5004
+ executable,
5005
+ nativeLimits(options2.env)
4838
5006
  );
4839
5007
  const executableFile = cachedExecutableName(executable);
4840
5008
  const executablePath = (0, import_node_path11.join)(temp, executableFile);
4841
- await (0, import_promises2.writeFile)(executablePath, executableBytes, { mode: 493 });
5009
+ await (0, import_promises3.writeFile)(executablePath, executableBytes, { mode: 493 });
4842
5010
  if ((options2.platform ?? import_node_process.default.platform) !== "win32") {
4843
- await (0, import_promises2.chmod)(executablePath, 493);
5011
+ await (0, import_promises3.chmod)(executablePath, 493);
4844
5012
  }
4845
5013
  const metadata = {
4846
5014
  schema: 2,
@@ -4849,7 +5017,7 @@ async function prepareExecutableFromArchive(archivePath, target, root, options2)
4849
5017
  executableSha256: sha256(executableBytes),
4850
5018
  executableFile
4851
5019
  };
4852
- await (0, import_promises2.writeFile)(
5020
+ await (0, import_promises3.writeFile)(
4853
5021
  (0, import_node_path11.join)(temp, "metadata.json"),
4854
5022
  JSON.stringify(metadata, null, 2) + "\n",
4855
5023
  { encoding: "utf8", mode: 384 }
@@ -4866,7 +5034,7 @@ async function prepareExecutableFromArchive(archivePath, target, root, options2)
4866
5034
  }
4867
5035
  return promoted;
4868
5036
  } finally {
4869
- await (0, import_promises2.rm)(temp, { recursive: true, force: true });
5037
+ await (0, import_promises3.rm)(temp, { recursive: true, force: true });
4870
5038
  }
4871
5039
  } finally {
4872
5040
  await lock.release();
@@ -4878,7 +5046,7 @@ async function prepareNativeExecutable(manifest, target, options2 = {}) {
4878
5046
  platform: options2.platform,
4879
5047
  home: options2.home
4880
5048
  });
4881
- await (0, import_promises2.mkdir)(root, { recursive: true });
5049
+ await (0, import_promises3.mkdir)(root, { recursive: true });
4882
5050
  const archivePath = await prepareNativeArchive(
4883
5051
  manifest,
4884
5052
  target,
@@ -4894,7 +5062,7 @@ async function prepareNativeExecutable(manifest, target, options2 = {}) {
4894
5062
  }
4895
5063
 
4896
5064
  // src/native/launcher/manifest.ts
4897
- var import_promises4 = require("node:fs/promises");
5065
+ var import_promises5 = require("node:fs/promises");
4898
5066
 
4899
5067
  // src/native/validate.ts
4900
5068
  var SUPPORTED_NATIVE_TARGETS = /* @__PURE__ */ new Set([
@@ -4926,6 +5094,13 @@ function parseNativeManifest(value) {
4926
5094
  }
4927
5095
  const version = requiredString(root.version, "manifest.version");
4928
5096
  const tag = requiredString(root.tag, "manifest.tag");
5097
+ let cacheEnv;
5098
+ if (root.cacheEnv !== void 0) {
5099
+ cacheEnv = requiredString(root.cacheEnv, "manifest.cacheEnv");
5100
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(cacheEnv)) {
5101
+ throw new Error("manifest.cacheEnv must be an environment variable name");
5102
+ }
5103
+ }
4929
5104
  if (!root.targets || typeof root.targets !== "object" || Array.isArray(root.targets)) {
4930
5105
  throw new Error("manifest.targets must be a mapping");
4931
5106
  }
@@ -4963,11 +5138,17 @@ function parseNativeManifest(value) {
4963
5138
  if (Object.keys(targets).length === 0) {
4964
5139
  throw new Error("manifest.targets must not be empty");
4965
5140
  }
4966
- return { repository, version, tag, targets };
5141
+ return {
5142
+ repository,
5143
+ version,
5144
+ tag,
5145
+ ...cacheEnv === void 0 ? {} : { cacheEnv },
5146
+ targets
5147
+ };
4967
5148
  }
4968
5149
  async function loadNativeManifest(manifestPath) {
4969
5150
  try {
4970
- const source = await (0, import_promises4.readFile)(manifestPath, "utf8");
5151
+ const source = await (0, import_promises5.readFile)(manifestPath, "utf8");
4971
5152
  return parseNativeManifest(JSON.parse(source));
4972
5153
  } catch (error) {
4973
5154
  throw new Error(
@@ -4978,7 +5159,7 @@ async function loadNativeManifest(manifestPath) {
4978
5159
 
4979
5160
  // src/native/launcher/target.ts
4980
5161
  var import_node_child_process = require("node:child_process");
4981
- var import_node_process2 = __toESM(require("node:process"));
5162
+ var import_node_process2 = __toESM(require("node:process"), 1);
4982
5163
  function linuxLibcFromReport(report) {
4983
5164
  if (!report || typeof report !== "object" || Array.isArray(report)) {
4984
5165
  return void 0;
package/README.md CHANGED
@@ -2,11 +2,17 @@
2
2
 
3
3
  Permanent integration fixture for releaseway/npm-actions.
4
4
 
5
- This package exists only to exercise real npm registry and GitHub Release behavior, including Trusted Publishing OIDC, exact-version reconciliation, staged publishing, and native distribution.
5
+ This package exercises real npm registry and GitHub Release behavior: registry-first version selection, Trusted Publishing OIDC, staged submissions, and native installation and execution. It is not intended for application dependencies or production use.
6
6
 
7
- It is not intended for application dependencies or production use.
7
+ The committed package remains an ordinary non-native fixture. Workflows rewrite checkout-local metadata without committing it. Both accept a full `action-ref` SHA, default to the published `releaseway/npm-actions v0.4.0` commit, verify the checked-out SHA and run `./npm-action`.
8
8
 
9
- The committed package remains an ordinary non-native fixture. Workflows may rewrite checkout-local metadata for an individual scenario before invoking npm-actions.
9
+ ## Result contract
10
+
11
+ `already-published` means the exact version was public at initial lookup. The action skips packaging and native Release validation for that version and makes no claim that current source equals the historical artifact.
12
+
13
+ `published` means a prepared candidate is live with the expected SHA-512, including an identical concurrent publication. `staged` means npm accepted a stage submission; the version is not yet confirmed public.
14
+
15
+ The caller selects a new version when new fixture source or a new action runtime needs to be published. Both workflows assert the returned package name, version, and expected state.
10
16
 
11
17
  ## Trusted Publishing
12
18
 
@@ -21,35 +27,51 @@ The bootstrap credential is no longer part of the fixture flow.
21
27
 
22
28
  ## Staged fixture
23
29
 
24
- `publish.yml` keeps the committed Releaseway policy at its staged default and exercises the real npm staged-publishing path.
30
+ `publish.yml` keeps the committed Releaseway policy at its staged default. It accepts `fixture-version` in the form `0.0.N-fixture.M` and `expected-state` as either `staged` or `already-published`.
31
+
32
+ The defaults use `0.0.5-fixture.0` with `already-published` to exercise version lookup without a new submission. To exercise actual staged publication, choose a fresh fixture version and `expected-state=staged`. Approval remains a maintainer operation. A pending staged version is not an already-public version, and a conflicting submission is expected to fail.
25
33
 
26
34
  ## Direct native fixture
27
35
 
28
- `direct.yml` reuses the existing direct Trusted Publisher connection and extends that scenario to the real native runtime path. Configure npm Trusted Publishing for:
36
+ `direct.yml` uses the direct Trusted Publisher connection. Configure npm Trusted Publishing for:
29
37
 
30
38
  - GitHub organization: `releaseway`
31
39
  - Repository: `npm-actions-fixture`
32
40
  - Workflow filename: `direct.yml`
33
41
  - Allowed action: enable `Allow npm publish`
34
42
 
35
- Repository release immutability must also be enabled before dispatching this workflow. The native workflow creates a draft Release, attaches the fixture asset, publishes the Release, and waits until GitHub reports that the Release is immutable and the asset has a SHA-256 digest.
43
+ The inputs default to `fixture-version=0.0.6-native.0` and `expected-state=already-published`. For this scenario the workflow skips Release creation and source-commit comparison, invokes npm-actions to verify the version is already public, then installs and executes the published CLI. It may run from a later source commit than the original Release. It does not republish the historical package or test a newly generated runtime.
44
+
45
+ To publish a new native fixture, select a fresh `0.0.N-native.M` version and `expected-state=published`. Repository release immutability must be enabled. The workflow creates or reuses a same-commit `native-v<version>` Release, uploads the Linux x64 asset, and waits for immutability and a SHA-256 asset digest before invoking npm-actions. The action prepares only an unpublished candidate and confirms its exact SHA-512 in the live registry.
46
+
47
+ Both native scenarios install the exact version with `--ignore-scripts` on Node 22, 24 and 26, each with a separate empty native cache. They execute the CLI, make its populated cache read-only, execute again and compare file snapshots. Fresh `published` runs also compare installed package-lock SHA-512 with the action report's frozen plan. Historical lookup remains a separate result.
48
+
49
+ The fixture asset is `native/fixture.sh`, packed into `npm-actions-native-fixture_linux_x64.tar.gz` at `bin/npm-actions-native-fixture`. It prints `npm-actions-native-fixture-ok` for the expected `--probe` invocation.
50
+
51
+ Cross-platform installed-wrapper behavior is covered in the npm-actions repository by the runtime/install matrix on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. This real-registry native fixture targets Linux x64 to exercise GitHub Release provenance, npm OIDC publication, installation, first-run download, and cache reuse.
36
52
 
37
- The workflow-local package version defaults to `0.0.6-native.0` and may be advanced with the `fixture-version` input. The committed `package.json` version is not changed by the workflow.
53
+ ## Candidate acceptance
38
54
 
39
- For a new native fixture version, `direct.yml` performs:
55
+ Supply the candidate's full 40-character `action-ref`, a fresh version and fresh
56
+ expected state (`staged` or `published`). New candidates must implement `report-path`.
57
+ Stable defaults continue to exercise historical lookup.
40
58
 
41
- 1. rewrite checkout-local package metadata to add the native bin and requested fixture version;
42
- 2. rewrite checkout-local Releaseway policy to direct mode with a Linux x64 GitHub Release distribution;
43
- 3. create or reuse the same-commit `native-v<version>` immutable Release;
44
- 4. run npm-actions through Trusted Publishing OIDC;
45
- 5. wait for the exact version and SHA-512 artifact to become live;
46
- 6. install that exact package version from npm using `--ignore-scripts`;
47
- 7. execute the installed native command once to populate the v2 runtime cache;
48
- 8. make the cache read-only and execute the command again;
49
- 9. verify that the cache file snapshot did not change on the second invocation.
59
+ Both workflows accept `version-source=package-json` (default) or `git-tag`. For tags,
60
+ create `npm-fixture-v<fixture-version>` targeting the fixture workflow commit before
61
+ dispatch. Use one matching version tag per fixture commit; ordinary and native
62
+ tag-derived versions need separate commits. Their dist-tags are `fixture` and `native`.
50
63
 
51
- The fixture asset itself is `native/fixture.sh`, packed into `npm-actions-native-fixture_linux_x64.tar.gz` with the executable path `bin/npm-actions-native-fixture`. It prints `npm-actions-native-fixture-ok` only for the expected `--probe` invocation.
64
+ Successful runs upload `releaseway-acceptance-<run_attempt>` with one `acceptance.json`,
65
+ retained for 30 days. It records candidate SHA, fixture commit, run/attempt, scenario,
66
+ version source, version, state, planned integrity for fresh publication and verified
67
+ Node versions for direct consumers. Staged success proves submission; approval and
68
+ installation remain separate maintainer actions. Historical lookup cannot certify a
69
+ new action release.
52
70
 
53
- Rerunning `direct.yml` with the same version and commit and `expected-state=existing` exercises exact-artifact reconciliation without another npm mutation. A native fixture version is intentionally not reusable from a different source commit because npm-actions requires the immutable Release tag to resolve to the current `GITHUB_SHA`.
71
+ Preserve a successful fresh publication run for readiness. If a version was already
72
+ submitted or published, choose a fresh version for another candidate test. Direct
73
+ publication resolves an identical concurrent publish through matching integrity;
74
+ a later rerun that initially sees the version public returns lookup state.
54
75
 
55
- Cross-platform installed-wrapper behavior is covered in the npm-actions repository by the hosted runtime/install matrix on Linux x64/arm64, macOS x64/arm64, and Windows x64/arm64. The real registry native fixture is intentionally Linux x64 so it can focus on GitHub Release provenance, npm OIDC publication, public installation, first-run download, and cache reuse.
76
+ Local contracts: `python3 test/contracts.py`. CI also checks shell syntax/workflows.
77
+ Public OIDC acceptance requires the Trusted Publisher connections described above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@releaseway/npm-actions-fixture",
3
- "version": "0.0.6-native.0",
3
+ "version": "0.0.9-native.0",
4
4
  "description": "Integration fixture for releaseway/npm-actions. Not for production use.",
5
5
  "type": "module",
6
6
  "exports": "./index.js",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "publishConfig": {
15
15
  "access": "public",
16
- "tag": "fixture"
16
+ "tag": "native"
17
17
  },
18
18
  "bin": {
19
19
  "npm-actions-native-fixture": "bin/npm-actions-native-fixture"