@releaseway/npm-actions-fixture 0.0.6-native.0 → 0.0.8-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.
- package/.releaseway/native.json +2 -2
- package/.releaseway/runtime.cjs +326 -145
- package/README.md +42 -20
- package/package.json +2 -2
package/.releaseway/native.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repository": "releaseway/npm-actions-fixture",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"tag": "native-v0.0.
|
|
3
|
+
"version": "0.0.8-native.0",
|
|
4
|
+
"tag": "native-v0.0.8-native.0",
|
|
5
5
|
"targets": {
|
|
6
6
|
"linux-x64-gnu": {
|
|
7
7
|
"asset": "npm-actions-native-fixture_linux_x64.tar.gz",
|
package/.releaseway/runtime.cjs
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
|
582
|
+
function open2(path, options2, callback) {
|
|
583
583
|
if (typeof options2 === "function") {
|
|
584
584
|
callback = options2;
|
|
585
585
|
options2 = null;
|
|
@@ -1391,13 +1391,93 @@ var import_node_process3 = __toESM(require("node:process"));
|
|
|
1391
1391
|
|
|
1392
1392
|
// src/native/launcher/cache.ts
|
|
1393
1393
|
var import_node_crypto3 = require("node:crypto");
|
|
1394
|
-
var
|
|
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
1398
|
var import_node_process = __toESM(require("node:process"));
|
|
1398
|
-
|
|
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
|
|
@@ -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
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
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
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
);
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
let
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
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
|
-
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
if (
|
|
4525
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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: () =>
|
|
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,
|
|
4879
|
+
const lockStat = await (0, import_promises3.stat)(lockDirectory);
|
|
4714
4880
|
if (Date.now() - lockStat.mtimeMs > staleMs) {
|
|
4715
|
-
await (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
4766
|
-
await (0,
|
|
4767
|
-
const temp = await (0,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
4792
|
-
await (0,
|
|
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,
|
|
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,
|
|
4832
|
-
const temp = await (0,
|
|
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,
|
|
5009
|
+
await (0, import_promises3.writeFile)(executablePath, executableBytes, { mode: 493 });
|
|
4842
5010
|
if ((options2.platform ?? import_node_process.default.platform) !== "win32") {
|
|
4843
|
-
await (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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 {
|
|
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,
|
|
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(
|
package/README.md
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Permanent integration fixture for releaseway/npm-actions.
|
|
4
4
|
|
|
5
|
-
This package
|
|
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
|
-
|
|
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 same stable reviewed commit, verify the checked-out SHA and run `./npm-action`.
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
## Candidate acceptance
|
|
38
54
|
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.8-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": "
|
|
16
|
+
"tag": "native"
|
|
17
17
|
},
|
|
18
18
|
"bin": {
|
|
19
19
|
"npm-actions-native-fixture": "bin/npm-actions-native-fixture"
|