fix: hand npm the release artifact's absolute path, so publishing finds it
CI / verify (pull_request) Successful in 1m5s
CI / infrastructure-smoke (pull_request) Successful in 37s
CI / migration-smoke (pull_request) Successful in 1m52s
CI / package-release (pull_request) Successful in 34s
CI / image-build (pull_request) Successful in 24s
CI / deploy-smoke (same-origin) (pull_request) Successful in 50s
CI / deploy-smoke (split) (pull_request) Successful in 1m30s
CI / package-publication (pull_request) Successful in 49s

The first real 0.2.0 publish failed on its first package with ENOENT:
"Publishing ekklesia-contracts-0.2.0.tgz failed: npm error code ENOENT".

The publish workflow passes a relative release path,
--release "$(find package-train …)", which is
package-train/ekklesia-packages-0.2.0. The tool validated the artifacts
relative to the job's working directory, but ran npm in its own
temporary home (cwd /tmp/ekklesia-publish-XXXXXX) with the same relative
path, package-train/ekklesia-packages-0.2.0/ekklesia-contracts-0.2.0.tgz,
which there names nothing. The disposable-registry suite always passed
an absolute path, so it never saw this.

- loadReleaseTrain resolves the release directory once; every artifact
  path is absolute from validation on, and npm is given only that exact
  file's absolute path (anything else is refused at the npm boundary).
- Just before each upload the file is checked against the validated
  bytes once more; the tool logs the absolute path it publishes, and a
  failure names it with npm's path/syscall lines (npm's temporary home
  masked, the token never shown).
- Regression tests: unit tests with a recording npm (relative nested
  release from another cwd; npm's argv, cwd, and the file's existence; a
  changed artifact refused before npm runs), which fail on the previous
  tool; and a disposable-Gitea test that runs the CLI exactly as the
  workflow does, from the Actions artifact layout, round-tripping all
  nine packages byte for byte.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-08 17:51:22 -05:00
co-authored by Claude Opus 5.5
parent debb8df3ce
commit 1caffc5f26
5 changed files with 207 additions and 17 deletions
+57 -3
View File
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { promisify } from "node:util";
@@ -28,8 +28,8 @@ const origin = setting("EKKLESIA_TEST_REGISTRY_ORIGIN");
const releaseDirectory = setting("EKKLESIA_TEST_RELEASE");
const publishToken = setting("EKKLESIA_TEST_PUBLISH_TOKEN");
const outsiderToken = setting("EKKLESIA_TEST_OUTSIDER_TOKEN");
const [mainOwner, burnedOwner, tagOwner] = setting("EKKLESIA_TEST_OWNERS").split(" ");
if (!origin || !releaseDirectory || !publishToken || !outsiderToken || !tagOwner) {
const [mainOwner, burnedOwner, tagOwner, layoutOwner] = setting("EKKLESIA_TEST_OWNERS").split(" ");
if (!origin || !releaseDirectory || !publishToken || !outsiderToken || !layoutOwner) {
throw new Error("Run this suite with scripts/test-package-publication.sh.");
}
const registryOf = (owner: string | undefined) =>
@@ -303,3 +303,57 @@ describe("moving the final tag, interrupted and resumed", () => {
}
});
});
describe("the publish workflow's own invocation", () => {
it("publishes from the Actions artifact layout, given a relative release path from another directory", async () => {
// Exactly as .github/workflows/publish-packages.yml runs it: the artifact unpacked under
// `package-train/` in the job's workspace, and `--release "$(find package-train …)"`.
const workspace = await mkdtemp(path.join(tmpdir(), "ekklesia-publish-workspace-"));
scratch.push(workspace);
const releaseName = path.basename(releaseDirectory);
await mkdir(path.join(workspace, "package-train"), { recursive: true });
await cp(releaseDirectory, path.join(workspace, "package-train", releaseName), {
recursive: true,
});
const record = path.join(workspace, "publication-record.json");
const { stdout } = await execFileAsync(
process.execPath,
[
path.resolve("scripts/publish-package-train.ts"),
"--release",
`package-train/${releaseName}`,
"--registry",
registryOf(layoutOwner),
"--version",
train.manifest.version,
"--revision",
train.manifest.source.revision,
"--manifest-sha256",
train.manifestSha256,
"--record",
record,
"--require-public-read",
"--allow-http",
],
{
cwd: workspace,
env: { [PUBLISH_TOKEN_VARIABLE]: publishToken, PATH: process.env.PATH ?? "" },
},
);
expect(stdout).toContain(
`publishing ${path.join(workspace, "package-train", releaseName, local("@ekklesia/contracts").entry.file)}`,
);
expect(stdout).not.toContain(publishToken);
const published = JSON.parse(await readFile(record, "utf8")) as PublicationRecord;
expect(published.packages.map((entry) => entry.outcome)).toEqual(Array(9).fill("published"));
for (const name of order) {
const version = (await anonymousPackument(layoutOwner, name))?.versions[
train.manifest.version
];
expect(
(await anonymousBytes(version?.dist.tarball ?? "")).equals(local(name).data),
name,
).toBe(true);
}
});
});