mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
fix(release): validate GITHUB_OUTPUT values and fail loudly on empty package lists
Hardens the new GITHUB_OUTPUT emission path so a malformed value can't smuggle extra `key=value` lines into the workflow's step outputs, and so the workflow's "Verify publish step emitted version" guard can't be fooled by a publish that did nothing. emitGithubOutputs now validates every key/value for `\n`/`\r` BEFORE the GITHUB_OUTPUT early-return — a malformed value is a caller bug and should fail loudly even when running locally. A multi-line value would need the heredoc form, which this helper deliberately does not support. prerelease.ts and publish-release.ts now fail loud when getPackagesForScope returns an empty list. Without this, the new GITHUB_OUTPUT emission would make the workflow's "Verify publish step emitted version" guard pass on a run that published nothing — previously the missing output made such a run fail. The guard runs BEFORE the dry-run branch in prerelease.ts. In publish-release.ts, the inline iteration of getPackagesForScope(scope) is hoisted to a `packages` const so the same guard fires before the publish loop. The "no-op when GITHUB_OUTPUT is unset" test now spies on fs.appendFileSync and asserts it wasn't called (the previous read of the unrelated temp file was vacuously true). New tests cover newline/CR in value and newline in key. The prerelease.ts usage string previously advertised `[--suffix <label>]`, but the script never parses --suffix (suffix handling lives in bump-prerelease.ts per the header comment). Removed. Call sites enumerated: - emitGithubOutputs: prerelease.ts (dry-run + post-publish), publish-release.ts - getPackagesForScope: prerelease.ts, publish-release.ts (this commit); bump-prerelease.ts, prepare-release.ts, versions.ts (not changed — out of scope for this hardening) Verification: - npx vitest run --config scripts/release/vitest.config.mts → 91 passed - Red-green for the newline validation: temporarily removed the validation, the 3 new newline/CR tests failed (assertion: expected fn to throw); restored, back to green. - E2E: GITHUB_OUTPUT="$OUT" pnpm release:prerelease:dry succeeded and the output file contained `version=1.59.5` and `scope=monorepo`. Note: Fix 2's empty-list guard fires only on a misconfigured scope (no unit test reachable — prerelease.ts is outside the vitest include glob and the guard is boundary validation against a misconfigured scope, not a behavior worth contriving a test harness for).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
@@ -22,6 +22,7 @@ afterEach(() => {
|
||||
process.env.GITHUB_OUTPUT = originalGithubOutput;
|
||||
}
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("emitGithubOutputs", () => {
|
||||
@@ -45,8 +46,25 @@ describe("emitGithubOutputs", () => {
|
||||
|
||||
it("is a no-op when GITHUB_OUTPUT is unset", () => {
|
||||
delete process.env.GITHUB_OUTPUT;
|
||||
const appendSpy = vi.spyOn(fs, "appendFileSync");
|
||||
|
||||
expect(() => emitGithubOutputs({ version: "1.2.3" })).not.toThrow();
|
||||
expect(fs.readFileSync(outputFile, "utf8")).toBe("");
|
||||
expect(appendSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws when a value contains a newline, naming the offending key", () => {
|
||||
expect(() =>
|
||||
emitGithubOutputs({ version: "1.2.3\nmalicious=evil" }),
|
||||
).toThrow(/version/);
|
||||
});
|
||||
|
||||
it("throws when a value contains a carriage return", () => {
|
||||
expect(() => emitGithubOutputs({ version: "1.2.3\r" })).toThrow(/version/);
|
||||
});
|
||||
|
||||
it("throws when a key contains a newline", () => {
|
||||
expect(() => emitGithubOutputs({ "bad\nkey": "value" })).toThrow(
|
||||
/bad\\nkey/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,8 +7,25 @@ import fs from "fs";
|
||||
* and the downstream summary/tag steps read `steps.publish.outputs.version`
|
||||
* (and `scope`), so every publish script must emit these after publishing.
|
||||
* No-op outside CI (GITHUB_OUTPUT unset), e.g. when running locally.
|
||||
*
|
||||
* Keys and values must be single-line: the helper throws if any contains a
|
||||
* newline or carriage return, since GITHUB_OUTPUT's `key=value` form cannot
|
||||
* carry newlines (multi-line values would need the heredoc form, which this
|
||||
* helper deliberately does not support).
|
||||
*/
|
||||
export function emitGithubOutputs(outputs: Record<string, string>): void {
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
if (/[\n\r]/.test(key)) {
|
||||
throw new Error(
|
||||
`emitGithubOutputs: key ${JSON.stringify(key)} contains a newline or carriage return; GITHUB_OUTPUT's key=value form cannot carry newlines (multi-line values would need the heredoc form, which this helper deliberately does not support).`,
|
||||
);
|
||||
}
|
||||
if (/[\n\r]/.test(value)) {
|
||||
throw new Error(
|
||||
`emitGithubOutputs: value for key ${JSON.stringify(key)} contains a newline or carriage return; GITHUB_OUTPUT's key=value form cannot carry newlines (multi-line values would need the heredoc form, which this helper deliberately does not support).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
if (!outputPath) return;
|
||||
const lines = Object.entries(outputs)
|
||||
|
||||
@@ -41,7 +41,7 @@ function main() {
|
||||
|
||||
if (!scope || !VALID_SCOPES.includes(scope)) {
|
||||
console.error(
|
||||
`Usage: prerelease.ts --scope <${VALID_SCOPES.join("|")}> [--suffix <label>] [--dry-run]`,
|
||||
`Usage: prerelease.ts --scope <${VALID_SCOPES.join("|")}> [--dry-run]`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -52,6 +52,12 @@ function main() {
|
||||
// Read the version from package.json — already bumped by bump-prerelease.ts
|
||||
// in the CI build job.
|
||||
const packages = getPackagesForScope(scope);
|
||||
if (packages.length === 0) {
|
||||
console.error(
|
||||
`No packages found for scope "${scope}" — refusing to emit a version for a publish that did nothing.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const publishVersion = packages[0]?.pkg.version ?? getCurrentVersion(scope);
|
||||
console.log(`Scope: ${scope}`);
|
||||
console.log(`Publishing version: ${publishVersion}`);
|
||||
|
||||
@@ -161,9 +161,16 @@ async function main() {
|
||||
// npm 11 uses GitHub Actions OIDC tokens for auth when id-token: write
|
||||
// is granted, eliminating the need for long-lived NPM_TOKEN secrets.
|
||||
// Skips packages already published at this version (idempotent retries).
|
||||
const packages = getPackagesForScope(scope);
|
||||
if (packages.length === 0) {
|
||||
console.error(
|
||||
`No packages found for scope "${scope}" — refusing to emit a version for a publish that did nothing.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\nPublishing packages...");
|
||||
let skipped = 0;
|
||||
for (const p of getPackagesForScope(scope)) {
|
||||
for (const p of packages) {
|
||||
const pubVersion = getPublishedVersion(p.name);
|
||||
if (pubVersion === version) {
|
||||
console.log(` Skipping ${p.name}@${version} (already published)`);
|
||||
|
||||
Reference in New Issue
Block a user