mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat: check what a push run publishes in preview-packages.mjs
Moves the push-run checks from scripts/preview-packages-push.sh into preview-packages.mjs, which reads the GitHub API with fetch: tests now feed raw API responses through the existing fake server, so a wrong field fails a test, and run in under 2s. The script also refuses a tag release whose CI did not pass, so that rule is tested too. The Find step fails on any passing push run whose artifact expired.
This commit is contained in:
parent
88b2a86547
commit
a7fdbbd250
5 changed files with 173 additions and 200 deletions
21
.github/workflows/preview-packages.yml
vendored
21
.github/workflows/preview-packages.yml
vendored
|
|
@ -19,15 +19,11 @@ on:
|
|||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
# The release tags preview-packages.mjs releases, as CI builds them (v*.*.*); a suffix marks a pre-release.
|
||||
RELEASE_TAG: '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Only CI runs for pushes to main, passing runs for release tags, and PRs from branches of this repo. A branch can
|
||||
# make its own copy of CI run on push; such runs are not reviewed and must not be published as push builds. A tag
|
||||
# run's head_branch is the tag name, which a branch can also have: scripts/preview-packages-push.sh verifies it.
|
||||
# run's head_branch is the tag name, which a branch can also have: preview-packages.mjs verifies push runs.
|
||||
if: >-
|
||||
github.event_name == 'workflow_run' && github.event.workflow_run.conclusion != 'cancelled' &&
|
||||
github.event.workflow_run.head_repository.full_name == github.repository &&
|
||||
|
|
@ -53,13 +49,13 @@ jobs:
|
|||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
EVENT: ${{ github.event.workflow_run.event }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
|
||||
run: |
|
||||
count=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" \
|
||||
--jq '[.artifacts[] | select(.name == "preview-packages")] | length')
|
||||
# Tag runs are only published when CI passed, so it packed: the artifact expired (after 3 days).
|
||||
if [ "$count" = 0 ] && [ "$EVENT" = push ] && [[ "$HEAD_BRANCH" =~ $RELEASE_TAG ]]; then
|
||||
echo "::error::The CI artifact of $HEAD_BRANCH expired; re-run the tag's CI to release it"
|
||||
# A passing push run packed, so its artifact expired (after 3 days); a release tag would go unreleased.
|
||||
if [ "$count" = 0 ] && [ "$EVENT" = push ] && [ "$CONCLUSION" = success ]; then
|
||||
echo "::error::The CI artifact expired; re-run CI to publish this commit"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$count" = 0 ]; then echo "::notice::CI produced no preview packages (it failed before packing)"; fi
|
||||
|
|
@ -89,16 +85,15 @@ jobs:
|
|||
EVENT: ${{ github.event.workflow_run.event }}
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
CONCLUSION: ${{ github.event.workflow_run.conclusion }}
|
||||
PULL_REQUESTS: ${{ toJSON(github.event.workflow_run.pull_requests) }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
DIR: ${{ runner.temp }}/preview-packages
|
||||
run: |
|
||||
args=(--dir "$DIR" --sha "$HEAD_SHA" --run "$RUN_ID" --event "$EVENT")
|
||||
if [ "$EVENT" = push ]; then
|
||||
# Checks the run may publish: only commits on main, under main or a release tag (see the script).
|
||||
push_args=$(bash scripts/preview-packages-push.sh)
|
||||
if [ -z "$push_args" ]; then exit 0; fi
|
||||
while IFS= read -r arg; do args+=("$arg"); done <<< "$push_args"
|
||||
# The script decides what a push run publishes (main or a release tag) and checks the commit is on main.
|
||||
args+=(--branch "$HEAD_BRANCH" --conclusion "$CONCLUSION")
|
||||
else
|
||||
# The branch's PR into main; a branch can also have PRs into other branches. It may have closed or received
|
||||
# new commits since CI ran, so the ref moves only while the PR is open with this commit as its head.
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Decides what a CI push run publishes to pkg.earendil.com, for .github/workflows/preview-packages.yml. Prints the
|
||||
# `preview-packages.mjs publish` arguments, one per line: `--ref main` for main, `--tag <tag>` and `--ref latest` for a
|
||||
# release tag (`--tag` only for a pre-release), nothing for any other name. Fails when the run must not publish.
|
||||
#
|
||||
# A tag push's head_branch is the tag's name, and a branch or tag can carry its own copy of CI. So a tag named `main`
|
||||
# or `v1.2.3` on an unreviewed commit would pass a name check: every push run must also be for a commit on main, and a
|
||||
# release tag must point at that commit.
|
||||
#
|
||||
# Environment: GITHUB_REPOSITORY, HEAD_SHA and HEAD_BRANCH of the CI run, RELEASE_TAG (the release tag pattern),
|
||||
# GH_TOKEN for `gh api`.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
repo="repos/$GITHUB_REPOSITORY"
|
||||
|
||||
# main is resolved as the branch: in compare, a tag named `main` would win over a bare `main`.
|
||||
require_on_main() {
|
||||
local main status
|
||||
main=$(gh api "$repo/git/ref/heads/main" --jq .object.sha)
|
||||
status=$(gh api "$repo/compare/$main...$HEAD_SHA" --jq .status)
|
||||
if [ "$status" != behind ] && [ "$status" != identical ]; then
|
||||
echo "::error::$HEAD_SHA is not on main; compare status: $status" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# The commit a tag points at. Annotated tags point at a tag object, which can point at another tag.
|
||||
tag_commit() {
|
||||
local ref type target
|
||||
ref=$(gh api "$repo/git/ref/tags/$1" --jq '"\(.object.type) \(.object.sha)"')
|
||||
read -r type target <<< "$ref"
|
||||
for _ in 1 2 3 4 5; do
|
||||
[ "$type" = tag ] || break
|
||||
ref=$(gh api "$repo/git/tags/$target" --jq '"\(.object.type) \(.object.sha)"')
|
||||
read -r type target <<< "$ref"
|
||||
done
|
||||
if [ "$type" != commit ]; then
|
||||
echo "::error::$1 does not resolve to a commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$target"
|
||||
}
|
||||
|
||||
if [ "$HEAD_BRANCH" = main ]; then
|
||||
require_on_main
|
||||
printf '%s\n' --ref main
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! [[ "$HEAD_BRANCH" =~ $RELEASE_TAG ]]; then
|
||||
echo "::notice::$HEAD_BRANCH is not a release tag; nothing to publish" >&2
|
||||
exit 0
|
||||
fi
|
||||
require_on_main
|
||||
target=$(tag_commit "$HEAD_BRANCH")
|
||||
if [ "$target" != "$HEAD_SHA" ]; then
|
||||
echo "::error::$HEAD_BRANCH points at $target, not $HEAD_SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' --tag "$HEAD_BRANCH"
|
||||
# A pre-release is kept under its tag, but `latest` stays on the newest release.
|
||||
if [[ "$HEAD_BRANCH" != *-* ]]; then printf '%s\n' --ref latest; fi
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const scripts = dirname(fileURLToPath(import.meta.url));
|
||||
const script = join(scripts, "preview-packages-push.sh");
|
||||
// The pattern the workflow passes, read from it so the test checks the real one.
|
||||
const workflow = readFileSync(join(scripts, "../.github/workflows/preview-packages.yml"), "utf8");
|
||||
const RELEASE_TAG = /^\s*RELEASE_TAG: '(.+)'$/m.exec(workflow)[1];
|
||||
const sha = "a".repeat(40);
|
||||
const mainSha = "m".repeat(40);
|
||||
const repo = "repos/earendil-works/pi";
|
||||
|
||||
/**
|
||||
* Runs the script for a CI push run against a fake `gh` that answers `gh api <path>` with `responses[path]`, already
|
||||
* filtered as by --jq, and fails like a 404 for any other path.
|
||||
*/
|
||||
async function decide(t, branch, responses) {
|
||||
const bin = mkdtempSync(join(tmpdir(), "pi-preview-packages-push-test-"));
|
||||
t.after(() => rmSync(bin, { recursive: true, force: true }));
|
||||
writeFileSync(join(bin, "responses.json"), JSON.stringify(responses));
|
||||
const fake = `#!/usr/bin/env node
|
||||
const responses = JSON.parse(require("node:fs").readFileSync(${JSON.stringify(join(bin, "responses.json"))}, "utf8"));
|
||||
const path = process.argv[3];
|
||||
if (!(path in responses)) { console.error("gh: Not Found (HTTP 404)"); process.exit(1); }
|
||||
console.log(responses[path]);
|
||||
`;
|
||||
writeFileSync(join(bin, "gh"), fake);
|
||||
chmodSync(join(bin, "gh"), 0o755);
|
||||
const env = {
|
||||
PATH: `${bin}:${process.env.PATH}`,
|
||||
GITHUB_REPOSITORY: "earendil-works/pi",
|
||||
HEAD_SHA: sha,
|
||||
HEAD_BRANCH: branch,
|
||||
RELEASE_TAG,
|
||||
};
|
||||
try {
|
||||
const { stdout } = await run("bash", [script], { env });
|
||||
return { args: stdout.split("\n").filter(Boolean) };
|
||||
} catch (error) {
|
||||
return { failed: error.stderr.trim() };
|
||||
}
|
||||
}
|
||||
|
||||
/** GitHub's answers for a commit on main: compare against main's SHA says how the commit relates to it. */
|
||||
function onMain(status = "behind") {
|
||||
return { [`${repo}/git/ref/heads/main`]: mainSha, [`${repo}/compare/${mainSha}...${sha}`]: status };
|
||||
}
|
||||
|
||||
test("publishes main and release tags of commits on main", async (t) => {
|
||||
const lightweight = { ...onMain(), [`${repo}/git/ref/tags/v1.2.3`]: `commit ${sha}` };
|
||||
assert.deepEqual(await decide(t, "main", onMain("identical")), { args: ["--ref", "main"] });
|
||||
assert.deepEqual(await decide(t, "v1.2.3", lightweight), { args: ["--tag", "v1.2.3", "--ref", "latest"] });
|
||||
|
||||
// An annotated tag can point at another annotated tag before the commit.
|
||||
const nested = {
|
||||
...onMain(),
|
||||
[`${repo}/git/ref/tags/v1.2.3`]: "tag t1",
|
||||
[`${repo}/git/tags/t1`]: "tag t2",
|
||||
[`${repo}/git/tags/t2`]: `commit ${sha}`,
|
||||
};
|
||||
assert.deepEqual(await decide(t, "v1.2.3", nested), { args: ["--tag", "v1.2.3", "--ref", "latest"] });
|
||||
|
||||
// A pre-release is released, but `latest` stays on the newest release.
|
||||
const prerelease = { ...onMain(), [`${repo}/git/ref/tags/v1.2.3-rc.1`]: `commit ${sha}` };
|
||||
assert.deepEqual(await decide(t, "v1.2.3-rc.1", prerelease), { args: ["--tag", "v1.2.3-rc.1"] });
|
||||
});
|
||||
|
||||
test("publishes nothing for names that are no release tag", async (t) => {
|
||||
for (const branch of ["vfoo", "v1.2", "release-1.2.3"]) {
|
||||
assert.deepEqual([branch, await decide(t, branch, onMain())], [branch, { args: [] }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("refuses commits off main, tags of other commits and failed checks", async (t) => {
|
||||
const tagged = { [`${repo}/git/ref/tags/v1.2.3`]: `commit ${sha}` };
|
||||
const cases = [
|
||||
// A tag named main on an unreviewed commit: a bare `main` in compare would resolve to the tag.
|
||||
["tag named main", "main", { ...onMain("diverged"), [`${repo}/compare/main...${sha}`]: "identical" }],
|
||||
["release tag off main", "v1.2.3", { ...onMain("ahead"), ...tagged }],
|
||||
["tag of another commit", "v1.2.3", { ...onMain(), [`${repo}/git/ref/tags/v1.2.3`]: `commit ${"b".repeat(40)}` }],
|
||||
["missing tag", "v1.2.3", onMain()],
|
||||
["main not found", "main", {}],
|
||||
["compare failed", "v1.2.3", { [`${repo}/git/ref/heads/main`]: mainSha, ...tagged }],
|
||||
];
|
||||
for (const [label, branch, responses] of cases) {
|
||||
const result = await decide(t, branch, responses);
|
||||
assert.ok(result.failed, `${label}: expected a refusal, got ${JSON.stringify(result)}`);
|
||||
}
|
||||
});
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
//
|
||||
// pack Point internal dependencies at this commit's builds, then `npm pack` every public package.
|
||||
// Runs in the regular CI job after the build.
|
||||
// publish Upload the packed tarballs, release a tag (v1.2.3) and move refs (main, latest, pr-1234). Runs in the
|
||||
// trusted workflow_run job and authenticates with the job's GitHub OIDC token
|
||||
// publish Upload the packed tarballs and move refs (main, pr-1234), or release a tag (v1.2.3) and move `latest`.
|
||||
// Runs in the trusted workflow_run job and authenticates with the job's GitHub OIDC token
|
||||
// (`permissions: id-token: write`). A release keeps the packages for good under the tag.
|
||||
// delete-ref Remove a ref, e.g. pr-1234 when the PR closes.
|
||||
|
||||
|
|
@ -17,6 +17,8 @@ import { getPublicWorkspacePackages } from "./release-packages.mjs";
|
|||
|
||||
const BASE_URL = "https://pkg.earendil.com";
|
||||
const FULL_SHA = /^[0-9a-f]{40}$/;
|
||||
// Release tags as CI builds them (v*.*.*); a suffix marks a pre-release (v1.2.3-rc.1).
|
||||
const RELEASE_TAG = /^v\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/;
|
||||
// Its daemon binaries are cross-compiled only by the release workflow; previews would ship without them.
|
||||
const EXCLUDED = new Set(["@earendil-works/pi-env"]);
|
||||
|
||||
|
|
@ -93,6 +95,46 @@ async function call(method, url, token, body) {
|
|||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
/** Reads `path` of this repository from the GitHub REST API with the job's token. */
|
||||
function github(path) {
|
||||
const api = process.env.GITHUB_API_URL ?? "https://api.github.com";
|
||||
return call("GET", `${api}/repos/${process.env.GITHUB_REPOSITORY}/${path}`, process.env.GH_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides what a CI push run publishes: `main`, a release tag with `latest` (without it for a pre-release), or nothing
|
||||
* for any other name. A tag push's branch is the tag's name, and a branch or tag can carry its own copy of CI, so a
|
||||
* tag named `main` or `v1.2.3` on an unreviewed commit would pass a name check: the commit must also be on main, and a
|
||||
* release tag must point at it.
|
||||
*/
|
||||
async function pushTarget({ branch, sha, conclusion }) {
|
||||
if (branch === "main") {
|
||||
await assertOnMain(sha);
|
||||
return { refs: ["main"] };
|
||||
}
|
||||
if (!RELEASE_TAG.test(branch)) return undefined;
|
||||
// CI packs before its tests run; a release is kept for good.
|
||||
if (conclusion !== "success") throw new Error(`CI for ${branch} did not pass; not releasing it`);
|
||||
await assertOnMain(sha);
|
||||
const target = await tagCommit(branch);
|
||||
if (target !== sha) throw new Error(`${branch} points at ${target}, not ${sha}`);
|
||||
return { tag: branch, refs: branch.includes("-") ? [] : ["latest"] };
|
||||
}
|
||||
|
||||
// main is resolved as the branch: in compare, a tag named `main` would win over a bare `main`.
|
||||
async function assertOnMain(sha) {
|
||||
const main = await github("git/ref/heads/main");
|
||||
const { status } = await github(`compare/${main.object.sha}...${sha}`);
|
||||
if (status !== "behind" && status !== "identical") throw new Error(`${sha} is not on main (${status})`);
|
||||
}
|
||||
|
||||
// Annotated tags point at a tag object, which can point at another tag.
|
||||
async function tagCommit(tag) {
|
||||
let { object } = await github(`git/ref/tags/${tag}`);
|
||||
while (object.type === "tag") ({ object } = await github(`git/tags/${object.sha}`));
|
||||
return object.sha;
|
||||
}
|
||||
|
||||
function renderSummary({ baseUrl, sha, release, refs, names }) {
|
||||
const lines = ["### Preview packages", "", `Built from \`${sha.slice(0, 7)}\`.`, ""];
|
||||
if (release) {
|
||||
|
|
@ -118,15 +160,16 @@ async function publish(argv) {
|
|||
options: {
|
||||
dir: { type: "string", default: "preview-packages" },
|
||||
sha: { type: "string" },
|
||||
tag: { type: "string" },
|
||||
ref: { type: "string", multiple: true, default: [] },
|
||||
branch: { type: "string" },
|
||||
conclusion: { type: "string" },
|
||||
run: { type: "string" },
|
||||
event: { type: "string" },
|
||||
"base-url": { type: "string", default: BASE_URL },
|
||||
},
|
||||
});
|
||||
assertSha(values.sha);
|
||||
const { sha, tag, ref: refs, event } = values;
|
||||
const { sha, event } = values;
|
||||
const run = Number(values.run);
|
||||
if (!/^\d+$/.test(values.run ?? "") || !Number.isSafeInteger(run)) {
|
||||
throw new UsageError(`--run must be the CI run ID, got ${values.run}`);
|
||||
|
|
@ -134,8 +177,16 @@ async function publish(argv) {
|
|||
if (event !== "push" && event !== "pull_request") {
|
||||
throw new UsageError(`--event must be push or pull_request, got ${event}`);
|
||||
}
|
||||
if (event === "push" && !values.branch) throw new UsageError("Push runs need --branch");
|
||||
const baseUrl = values["base-url"];
|
||||
const dir = resolve(values.dir);
|
||||
// Push runs are checked before anything is uploaded; pull request runs move the refs the workflow passes.
|
||||
const target = event === "push" ? await pushTarget(values) : { refs: values.ref };
|
||||
if (!target) {
|
||||
console.error(`::notice::${values.branch} is neither main nor a release tag; nothing to publish`);
|
||||
return;
|
||||
}
|
||||
const { tag, refs } = target;
|
||||
|
||||
// The artifact comes from an untrusted PR build: only read tarballs from it. The SHA, run, event, and refs
|
||||
// come from the trusted workflow_run event; the worker authorizes package names against its config.
|
||||
|
|
@ -188,8 +239,10 @@ async function deleteRef(argv) {
|
|||
|
||||
const USAGE = `Usage:
|
||||
node scripts/preview-packages.mjs pack --sha <sha> [--out dir]
|
||||
node scripts/preview-packages.mjs publish --sha <sha> --run <CI run id> --event <push|pull_request>
|
||||
[--dir dir] [--tag name] [--ref name]...
|
||||
node scripts/preview-packages.mjs publish --sha <sha> --run <CI run id> --event push --branch <name>
|
||||
--conclusion <CI conclusion> [--dir dir]
|
||||
node scripts/preview-packages.mjs publish --sha <sha> --run <CI run id> --event pull_request
|
||||
[--dir dir] [--ref name]...
|
||||
node scripts/preview-packages.mjs delete-ref --ref <name>`;
|
||||
|
||||
const [command, ...argv] = process.argv.slice(2);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ const run = promisify(execFile);
|
|||
const script = join(dirname(fileURLToPath(import.meta.url)), "preview-packages.mjs");
|
||||
const sha = "a".repeat(40);
|
||||
const newerSha = "b".repeat(40);
|
||||
const mainSha = "c".repeat(40);
|
||||
const REPO_API = "/repos/earendil-works/pi/";
|
||||
|
||||
function temporaryDirectory(t) {
|
||||
const directory = mkdtempSync(join(tmpdir(), "pi-preview-packages-test-"));
|
||||
|
|
@ -24,12 +26,23 @@ function writePackage(repo, directory, manifest) {
|
|||
writeFileSync(join(repo, "packages", directory, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
|
||||
}
|
||||
|
||||
/** GitHub's answers for a commit on main: compare against main's SHA says how the commit relates to main. */
|
||||
function onMain(status = "behind") {
|
||||
return { "git/ref/heads/main": { object: { sha: mainSha } }, [`compare/${mainSha}...${sha}`]: { status } };
|
||||
}
|
||||
|
||||
/** GitHub's answers for a release tag of the commit on main. */
|
||||
function released(tag) {
|
||||
return { ...onMain(), [`git/ref/tags/${tag}`]: { object: { type: "commit", sha } } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Stands in for the pkg.earendil.com worker and GitHub's OIDC token endpoint, recording requests. `refsSha` is what the
|
||||
* worker reports each ref resolves to after the update; `uploadBody` overrides the upload response; `releaseStatus`
|
||||
* the release's status.
|
||||
* Stands in for the pkg.earendil.com worker, GitHub's OIDC token endpoint and GitHub's REST API, recording requests.
|
||||
* `refsSha` is what the worker reports each ref resolves to after the update; `uploadBody` overrides the upload
|
||||
* response; `releaseStatus` the release's status. `github` maps paths below the repository to API responses; any other
|
||||
* API path is a 404.
|
||||
*/
|
||||
async function startServer(t, { refsSha = sha, uploadBody, releaseStatus = 201 } = {}) {
|
||||
async function startServer(t, { refsSha = sha, uploadBody, releaseStatus = 201, github = {} } = {}) {
|
||||
const requests = [];
|
||||
const server = createServer(async (req, res) => {
|
||||
let body = "";
|
||||
|
|
@ -37,6 +50,11 @@ async function startServer(t, { refsSha = sha, uploadBody, releaseStatus = 201 }
|
|||
requests.push({ method: req.method, url: req.url, authorization: req.headers.authorization, body });
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
if (url.pathname === "/token") return res.end(JSON.stringify({ value: `oidc-for-${url.searchParams.get("audience")}` }));
|
||||
if (url.pathname.startsWith(REPO_API)) {
|
||||
const response = github[url.pathname.slice(REPO_API.length)];
|
||||
res.statusCode = response ? 200 : 404;
|
||||
return res.end(JSON.stringify(response ?? { message: "Not Found" }));
|
||||
}
|
||||
if (req.method === "PUT") {
|
||||
const name = decodeURIComponent(url.pathname.split("/").slice(4).join("/"));
|
||||
return res.end(uploadBody ?? JSON.stringify({ name, sha, existing: false }));
|
||||
|
|
@ -66,10 +84,28 @@ function publish(server, dir, args = ["--event", "pull_request", "--ref", "pr-1"
|
|||
const { GITHUB_STEP_SUMMARY: _, ...env } = process.env;
|
||||
const common = ["publish", "--sha", sha, "--run", "7", "--dir", dir];
|
||||
return run(process.execPath, [script, ...common, ...args, "--base-url", server.url], {
|
||||
env: { ...env, ACTIONS_ID_TOKEN_REQUEST_URL: `${server.url}/token`, ACTIONS_ID_TOKEN_REQUEST_TOKEN: "runner" },
|
||||
env: {
|
||||
...env,
|
||||
ACTIONS_ID_TOKEN_REQUEST_URL: `${server.url}/token`,
|
||||
ACTIONS_ID_TOKEN_REQUEST_TOKEN: "runner",
|
||||
GITHUB_API_URL: server.url,
|
||||
GITHUB_REPOSITORY: "earendil-works/pi",
|
||||
GH_TOKEN: "job-token",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** The arguments of a CI push run of `branch`. */
|
||||
function pushRun(branch, conclusion = "success") {
|
||||
return ["--event", "push", "--branch", branch, "--conclusion", conclusion];
|
||||
}
|
||||
|
||||
/** The POST requests to pkg.earendil.com, as [path, body]. */
|
||||
function posts(server) {
|
||||
const requests = server.requests.filter((request) => request.method === "POST");
|
||||
return requests.map((request) => [request.url, JSON.parse(request.body)]);
|
||||
}
|
||||
|
||||
test("pack points internal dependencies at the commit's builds and restores package.json", async (t) => {
|
||||
const repo = temporaryDirectory(t);
|
||||
writePackage(repo, "a", { name: "@scope/a", version: "1.0.0", dependencies: { "@scope/b": "1.0.0", other: "2.0.0" } });
|
||||
|
|
@ -133,42 +169,90 @@ test("publish fails on a successful response that is not JSON", async (t) => {
|
|||
assert.equal(server.requests.filter((request) => request.method === "POST").length, 0);
|
||||
});
|
||||
|
||||
const RELEASE_ARGS = ["--event", "push", "--tag", "v1.2.3", "--ref", "latest"];
|
||||
test("publish decides what a push run publishes", async (t) => {
|
||||
const release = (tag) => ["/_/releases", { sha, tag }];
|
||||
const latest = ["/_/refs", { sha, refs: ["latest"], run: 7 }];
|
||||
const nested = {
|
||||
...onMain(),
|
||||
"git/ref/tags/v1.2.3": { object: { type: "tag", sha: "t1" } },
|
||||
"git/tags/t1": { object: { type: "tag", sha: "t2" } },
|
||||
"git/tags/t2": { object: { type: "commit", sha } },
|
||||
};
|
||||
const cases = [
|
||||
["main", onMain("identical"), [["/_/refs", { sha, refs: ["main"], run: 7 }]]],
|
||||
["v1.2.3", released("v1.2.3"), [release("v1.2.3"), latest]],
|
||||
// An annotated tag can point at another annotated tag before the commit.
|
||||
["v1.2.3", nested, [release("v1.2.3"), latest]],
|
||||
// A pre-release is kept under its tag, but `latest` stays on the newest release.
|
||||
["v1.2.3-beta.1", released("v1.2.3-beta.1"), [release("v1.2.3-beta.1")]],
|
||||
];
|
||||
for (const [branch, github, expected] of cases) {
|
||||
const server = await startServer(t, { github });
|
||||
const dir = join(temporaryDirectory(t), "artifact");
|
||||
writeArtifact(dir);
|
||||
await publish(server, dir, pushRun(branch));
|
||||
assert.deepEqual([branch, posts(server)], [branch, expected]);
|
||||
}
|
||||
});
|
||||
|
||||
test("publish releases a tag before moving refs and prints its install commands", async (t) => {
|
||||
const server = await startServer(t);
|
||||
test("publish uploads nothing for push runs of other names", async (t) => {
|
||||
for (const branch of ["vfoo", "v1.2", "v1.2.3.4", "release-v1.2.3", "v1.2.3-"]) {
|
||||
const server = await startServer(t, { github: released(branch) });
|
||||
const dir = join(temporaryDirectory(t), "artifact");
|
||||
writeArtifact(dir);
|
||||
const { stderr } = await publish(server, dir, pushRun(branch));
|
||||
assert.match(stderr, /nothing to publish/);
|
||||
assert.deepEqual([branch, server.requests], [branch, []]);
|
||||
}
|
||||
});
|
||||
|
||||
test("publish refuses push runs off main, of other commits or with failing CI", async (t) => {
|
||||
const otherCommit = { ...onMain(), "git/ref/tags/v1.2.3": { object: { type: "commit", sha: newerSha } } };
|
||||
const cases = [
|
||||
// A tag named main on an unreviewed commit: a bare `main` in compare would resolve to the tag.
|
||||
["tag named main", "main", "success", { ...onMain("diverged"), [`compare/main...${sha}`]: { status: "identical" } }],
|
||||
["release tag off main", "v1.2.3", "success", { ...released("v1.2.3"), ...onMain("ahead") }],
|
||||
["tag of another commit", "v1.2.3", "success", otherCommit],
|
||||
["missing tag", "v1.2.3", "success", onMain()],
|
||||
["failing CI", "v1.2.3", "failure", released("v1.2.3")],
|
||||
];
|
||||
for (const [label, branch, conclusion, github] of cases) {
|
||||
const server = await startServer(t, { github });
|
||||
const dir = join(temporaryDirectory(t), "artifact");
|
||||
writeArtifact(dir);
|
||||
await assert.rejects(publish(server, dir, pushRun(branch, conclusion)), undefined, label);
|
||||
const uploads = server.requests.filter((request) => request.method === "PUT");
|
||||
assert.deepEqual([label, uploads], [label, []]);
|
||||
}
|
||||
});
|
||||
|
||||
const RELEASE_RUN = pushRun("v1.2.3");
|
||||
|
||||
test("publish prints the release's install commands", async (t) => {
|
||||
const server = await startServer(t, { github: released("v1.2.3") });
|
||||
const dir = join(temporaryDirectory(t), "artifact");
|
||||
writeArtifact(dir);
|
||||
|
||||
const { stdout } = await publish(server, dir, RELEASE_ARGS);
|
||||
|
||||
const posts = server.requests.filter((request) => request.method === "POST");
|
||||
assert.deepEqual(
|
||||
posts.map((request) => [request.url, JSON.parse(request.body)]),
|
||||
[
|
||||
["/_/releases", { sha, tag: "v1.2.3" }],
|
||||
["/_/refs", { sha, refs: ["latest"], run: 7 }],
|
||||
],
|
||||
);
|
||||
const { stdout } = await publish(server, dir, RELEASE_RUN);
|
||||
assert.match(stdout, new RegExp(`npm i ${server.url}/@scope/a@v1\\.2\\.3`));
|
||||
assert.match(stdout, new RegExp(`npm i ${server.url}/@scope/a@latest`));
|
||||
});
|
||||
|
||||
test("publish moves no refs when the release fails", async (t) => {
|
||||
const server = await startServer(t, { releaseStatus: 409 });
|
||||
const server = await startServer(t, { releaseStatus: 409, github: released("v1.2.3") });
|
||||
const dir = join(temporaryDirectory(t), "artifact");
|
||||
writeArtifact(dir);
|
||||
|
||||
await assert.rejects(publish(server, dir, RELEASE_ARGS), /409/);
|
||||
await assert.rejects(publish(server, dir, RELEASE_RUN), /409/);
|
||||
assert.equal(server.requests.filter((request) => request.url === "/_/refs").length, 0);
|
||||
});
|
||||
|
||||
test("publish still prints the release when a newer release owns latest", async (t) => {
|
||||
const server = await startServer(t, { refsSha: newerSha });
|
||||
const server = await startServer(t, { refsSha: newerSha, github: released("v1.2.3") });
|
||||
const dir = join(temporaryDirectory(t), "artifact");
|
||||
writeArtifact(dir);
|
||||
|
||||
const { stdout } = await publish(server, dir, RELEASE_ARGS);
|
||||
const { stdout } = await publish(server, dir, RELEASE_RUN);
|
||||
assert.match(stdout, /@scope\/a@v1\.2\.3/);
|
||||
assert.doesNotMatch(stdout, /@latest/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue