mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
169 lines
6.0 KiB
JavaScript
169 lines
6.0 KiB
JavaScript
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { describe, it } = require("node:test");
|
|
|
|
const repoRoot = path.resolve(__dirname, "..");
|
|
const releaseWorkflow = fs.readFileSync(
|
|
path.join(repoRoot, ".github/workflows/release.yml"),
|
|
"utf8",
|
|
);
|
|
const previewWorkflow = fs.readFileSync(
|
|
path.join(repoRoot, ".github/workflows/pkg-pr-new.yml"),
|
|
"utf8",
|
|
);
|
|
function topLevelBlock(source, name) {
|
|
const match = source.match(
|
|
new RegExp(
|
|
`^${name}:\\n([\\s\\S]*?)(?=^[A-Za-z][A-Za-z0-9_-]*:|(?![\\s\\S]))`,
|
|
"m",
|
|
),
|
|
);
|
|
assert.ok(match, `missing top-level ${name} block`);
|
|
return match[0];
|
|
}
|
|
|
|
function jobBlock(source, name) {
|
|
const jobs = topLevelBlock(source, "jobs");
|
|
const match = jobs.match(
|
|
new RegExp(
|
|
`^ ${name}:\\n([\\s\\S]*?)(?=^ [A-Za-z][A-Za-z0-9_-]*:|(?![\\s\\S]))`,
|
|
"m",
|
|
),
|
|
);
|
|
assert.ok(match, `missing ${name} job`);
|
|
return match[0];
|
|
}
|
|
|
|
function assertInOrder(source, snippets) {
|
|
let previous = -1;
|
|
for (const snippet of snippets) {
|
|
const index = source.indexOf(snippet);
|
|
assert.ok(index >= 0, `missing workflow fragment: ${snippet}`);
|
|
assert.ok(index > previous, `workflow fragment is out of order: ${snippet}`);
|
|
previous = index;
|
|
}
|
|
}
|
|
|
|
function permissionLines(job) {
|
|
const match = job.match(/^ permissions:\n((?: .+\n)+)/m);
|
|
assert.ok(match, "missing job permissions");
|
|
return match[1].trim().split("\n").map((line) => line.trim()).sort();
|
|
}
|
|
|
|
describe("release workflow contract", () => {
|
|
it("has only the version-tag production trigger", () => {
|
|
const trigger = topLevelBlock(releaseWorkflow, "on");
|
|
|
|
assert.match(trigger, /^on:\n push:\n tags:\n - 'v\*'\n+$/);
|
|
for (const forbidden of [
|
|
"workflow_dispatch:",
|
|
"workflow_run:",
|
|
"pull_request:",
|
|
"pull_request_target:",
|
|
]) {
|
|
assert.equal(releaseWorkflow.includes(forbidden), false, forbidden);
|
|
}
|
|
});
|
|
|
|
it("runs preflight before every release side effect", () => {
|
|
const preflight = jobBlock(releaseWorkflow, "preflight");
|
|
|
|
assert.deepEqual(permissionLines(preflight), ["contents: read"]);
|
|
assertInOrder(preflight, [
|
|
"actions/checkout@",
|
|
"fetch-depth: 0",
|
|
"actions/setup-node@",
|
|
"node-version: '22.14.0'",
|
|
"node scripts/release-preflight.js --tag \"$TAG\"",
|
|
"git rev-parse --verify 'HEAD^{commit}'",
|
|
"git rev-parse --verify \"refs/tags/${TAG}^{commit}\"",
|
|
'if [[ "$TAG" == *-beta.* ]]',
|
|
'git fetch origin "$REHEARSAL_BRANCH"',
|
|
"git rev-parse --verify 'FETCH_HEAD^{commit}'",
|
|
"git fetch origin main",
|
|
'git merge-base --is-ancestor "$HEAD_SHA" "$MAIN_SHA"',
|
|
]);
|
|
assert.equal(preflight.includes("gh release"), false);
|
|
assert.equal(preflight.includes("npm publish"), false);
|
|
});
|
|
|
|
it("builds a verified staging asset before approval", () => {
|
|
const build = jobBlock(releaseWorkflow, "build-stage-assets");
|
|
|
|
assert.match(build, /needs: preflight/);
|
|
assert.deepEqual(permissionLines(build), ["contents: read"]);
|
|
assert.doesNotMatch(build, /^ environment:/m);
|
|
assert.match(build, /actions\/setup-go@[0-9a-f]{40}/);
|
|
assert.match(build, /actions\/setup-python@[0-9a-f]{40}/);
|
|
assert.match(
|
|
build,
|
|
/actions\/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6/,
|
|
);
|
|
assert.match(build, /node-version: '22.14.0'/);
|
|
assert.match(build, /registry-url: 'https:\/\/registry\.npmjs\.org'/);
|
|
assert.match(build, /package-manager-cache: false/);
|
|
assert.match(build, /npm install --global npm@11\.16\.0/);
|
|
assert.match(build, /goreleaser\/goreleaser-action@[0-9a-f]{40}/);
|
|
assert.match(build, /args: release --clean --skip=publish/);
|
|
assertInOrder(build, [
|
|
"actions/setup-go@",
|
|
"actions/setup-python@",
|
|
"actions/setup-node@",
|
|
"npm install --global npm@11.16.0",
|
|
"goreleaser/goreleaser-action@",
|
|
"sha256sum --check checksums.txt",
|
|
"cp dist/checksums.txt checksums.txt",
|
|
"npm pack --ignore-scripts --json",
|
|
"tar -tzf \"$PACK_FILE\" | grep -qx 'package/checksums.txt'",
|
|
"actions/upload-artifact@",
|
|
]);
|
|
assert.equal(build.includes("npm stage publish"), false);
|
|
});
|
|
|
|
it("limits the protected job to verifying and staging the prepared npm asset", () => {
|
|
const publish = jobBlock(releaseWorkflow, "stage-publish");
|
|
|
|
assert.match(publish, /needs: build-stage-assets/);
|
|
assert.deepEqual(permissionLines(publish), ["id-token: write"]);
|
|
assert.match(publish, /^ environment: npm-production$/m);
|
|
assert.doesNotMatch(publish, /actions\/checkout@/);
|
|
assert.doesNotMatch(publish, /goreleaser\/goreleaser-action@/);
|
|
assert.doesNotMatch(publish, /GITHUB_TOKEN:/);
|
|
assertInOrder(publish, [
|
|
"actions/setup-node@",
|
|
"npm install --global npm@11.16.0",
|
|
"actions/download-artifact@",
|
|
"sha256sum --check checksums.txt",
|
|
"tar -tzf \"$PACK_FILE\" | grep -qx 'package/checksums.txt'",
|
|
'npm stage publish "${{ steps.asset.outputs.filename }}" --access public --tag beta',
|
|
]);
|
|
for (const forbidden of [
|
|
"gh release download",
|
|
"npm view",
|
|
"LOCAL_INTEGRITY",
|
|
"REMOTE_INTEGRITY",
|
|
"secrets.NPM_TOKEN",
|
|
"NODE_AUTH_TOKEN",
|
|
"GITHUB_TOKEN:",
|
|
"gh release create",
|
|
]) {
|
|
assert.equal(releaseWorkflow.includes(forbidden), false, forbidden);
|
|
}
|
|
assert.equal(/(^|\s)npm publish(?:\s|$)/m.test(publish), false);
|
|
});
|
|
});
|
|
|
|
describe("preview isolation", () => {
|
|
it("keeps preview publishing away from production credentials and registry", () => {
|
|
assert.equal(previewWorkflow.includes("id-token: write"), false);
|
|
assert.equal(previewWorkflow.includes("npm publish"), false);
|
|
assert.equal(previewWorkflow.includes("registry.npmjs.org"), false);
|
|
assert.equal(previewWorkflow.includes("secrets.NPM_TOKEN"), false);
|
|
assert.equal(previewWorkflow.includes("NODE_AUTH_TOKEN"), false);
|
|
});
|
|
});
|