2026-07-15 20:12:56 -03:00

133 lines
3.7 KiB
JavaScript

import {
appendFile,
lstat,
readFile,
realpath,
rm,
statfs,
writeFile,
} from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { DefaultArtifactClient } from "@actions/artifact";
const GIB = 1024 ** 3;
const MIB = 1024 ** 2;
function humanSize(size) {
if (size >= GIB) return `${(size / GIB).toFixed(2)} GiB`;
return `${(size / MIB).toFixed(2)} MiB`;
}
export async function buildAndUploadIntegrationTargets({ core, exec }) {
const artifactDirectory = await realpath(
resolve(process.env.INTEGRATION_ARTIFACT_DIR ?? ""),
);
const manifest = resolve(process.env.INTEGRATION_ARTIFACT_MANIFEST ?? "");
const targetsPath = resolve(process.env.INTEGRATION_TARGETS_FILE ?? "");
if (dirname(manifest) !== artifactDirectory) {
throw new Error(`integration manifest is outside staging: ${manifest}`);
}
if (dirname(targetsPath) !== artifactDirectory) {
throw new Error(`integration target list is outside staging: ${targetsPath}`);
}
await writeFile(manifest, "");
const targets = (await readFile(targetsPath, "utf8"))
.split("\n")
.filter(Boolean);
if (targets.length === 0 || targets.length > 64) {
throw new Error(`invalid integration target count: ${targets.length}`);
}
const artifact = new DefaultArtifactClient();
let totalArchiveSize = 0;
for (const target of targets) {
if (!/^[a-z0-9_]+$/.test(target)) {
throw new Error(`invalid integration target: ${target}`);
}
const artifactName = `integration-test-${target}.tar.zst`;
const archive = join(artifactDirectory, artifactName);
try {
const exitCode = await exec.exec("cargo", [
"nextest",
"archive",
"-p",
"integration_tests",
"--test",
target,
"-E",
`binary(=${target})`,
"--archive-file",
archive,
"--no-pager",
]);
if (exitCode !== 0) {
throw new Error(`nextest archive failed for ${target}`);
}
const archiveStat = await lstat(archive);
if (!archiveStat.isFile()) {
throw new Error(`integration artifact is not a file: ${archive}`);
}
if (archiveStat.size > 128 * MIB) {
throw new Error(`${target} archive exceeds the 128 MiB limit`);
}
totalArchiveSize += archiveStat.size;
if (totalArchiveSize > 4 * GIB) {
throw new Error("integration archives exceed the 4 GiB total limit");
}
await appendFile(
process.env.GITHUB_STEP_SUMMARY,
`| \`${target}\` | ${humanSize(archiveStat.size)} |\n`,
);
const { artifacts } = await artifact.listArtifacts({ latest: true });
if (artifacts.some(({ name }) => name === artifactName)) {
await artifact.deleteArtifact(artifactName);
}
const { digest, id } = await artifact.uploadArtifact(
artifactName,
[archive],
artifactDirectory,
{
retentionDays: 1,
skipArchive: true,
},
);
if (!digest || !id) {
throw new Error(`upload returned incomplete metadata: ${artifactName}`);
}
await appendFile(
manifest,
`${JSON.stringify({
archive: artifactName,
artifact_id: id,
target,
})}\n`,
);
core.info(`Uploaded ${artifactName} (artifact ${id}, digest ${digest}).`);
} finally {
await rm(archive, { force: true });
}
const disk = await statfs(artifactDirectory);
if (disk.bavail * disk.bsize < 6 * GIB) {
throw new Error("less than 6 GiB remains while building archives");
}
}
await appendFile(
process.env.GITHUB_STEP_SUMMARY,
`\nBuilt ${targets.length} integration targets exactly once.\n`,
);
}