Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 90 additions & 9 deletions .github/scripts/__tests__/push-changes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ const {

describe("artifact branch helpers", () => {
it("normalizes source branch names", () => {
assert.equal(normalizeBranchName("refs/heads/feat/example"), "feat/example");
assert.equal(
normalizeBranchName("refs/heads/feat/example"),
"feat/example",
);
assert.equal(normalizeBranchName("feat/example"), "feat/example");
});

Expand All @@ -24,27 +27,68 @@ describe("artifact branch helpers", () => {
});

it("maps source branches to dist artifact branches", () => {
assert.equal(deriveArtifactRef("feat/example", "dist/"), "dist/feat/example");
assert.equal(
deriveArtifactRef("feat/example", "dist/"),
"dist/feat/example",
);
assert.equal(deriveArtifactRef("main", "dist"), "dist/main");
});

it("does not double-prefix dist branches", () => {
assert.equal(deriveArtifactRef("dist/feat/example", "dist/"), "dist/feat/example");
assert.equal(
deriveArtifactRef("dist/feat/example", "dist/"),
"dist/feat/example",
);
});
});

describe("collectTreeEntries", () => {
function createWorkspace({ withActionYaml = false, withComputeWorkflow = false } = {}) {
function createWorkspace({
withActionYaml = false,
withComputeWorkflow = false,
withPackageJson = false,
withBunLock = false,
withBunLockBinary = false,
} = {}) {
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "push-changes-"));
fs.mkdirSync(path.join(workspace, "dist"), { recursive: true });
fs.writeFileSync(path.join(workspace, "manifest.json"), '{"name":"fixture"}\n');
fs.writeFileSync(path.join(workspace, "dist", "index.js"), "console.log('artifact');\n");
fs.writeFileSync(
path.join(workspace, "manifest.json"),
'{"name":"fixture"}\n',
);
fs.writeFileSync(
path.join(workspace, "dist", "index.js"),
"console.log('artifact');\n",
);
if (withActionYaml) {
fs.writeFileSync(path.join(workspace, "action.yml"), "name: fixture-action\n");
fs.writeFileSync(
path.join(workspace, "action.yml"),
"name: fixture-action\n",
);
}
if (withComputeWorkflow) {
fs.mkdirSync(path.join(workspace, ".github", "workflows"), { recursive: true });
fs.writeFileSync(path.join(workspace, ".github", "workflows", "compute.yml"), "name: Compute\n");
fs.mkdirSync(path.join(workspace, ".github", "workflows"), {
recursive: true,
});
fs.writeFileSync(
path.join(workspace, ".github", "workflows", "compute.yml"),
"name: Compute\n",
);
}
if (withPackageJson) {
fs.writeFileSync(
path.join(workspace, "package.json"),
'{"name":"fixture","private":true}\n',
);
}
if (withBunLock) {
fs.writeFileSync(path.join(workspace, "bun.lock"), "# bun lockfile v1\n");
}
if (withBunLockBinary) {
fs.writeFileSync(
path.join(workspace, "bun.lockb"),
Buffer.from([0, 1, 2, 3]),
);
}
return workspace;
}
Expand Down Expand Up @@ -77,6 +121,8 @@ describe("collectTreeEntries", () => {
assert.ok(entryPaths.includes("dist/index.js"));
assert.ok(!entryPaths.includes("action.yml"));
assert.ok(!entryPaths.includes(".github/workflows/compute.yml"));
assert.ok(!entryPaths.includes("package.json"));
assert.ok(!entryPaths.includes("bun.lock"));
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
Expand All @@ -97,4 +143,39 @@ describe("collectTreeEntries", () => {
fs.rmSync(workspace, { recursive: true, force: true });
}
});

it("includes package.json and bun.lock when present", () => {
const workspace = createWorkspace({
withPackageJson: true,
withBunLock: true,
});
try {
const entries = collectTreeEntries({
githubWorkspace: workspace,
manifestPathInput: "manifest.json",
});
const entryPaths = entries.map((entry) => entry.path);
assert.ok(entryPaths.includes("manifest.json"));
assert.ok(entryPaths.includes("dist/index.js"));
assert.ok(entryPaths.includes("package.json"));
assert.ok(entryPaths.includes("bun.lock"));
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});

it("encodes bun.lockb as base64 when present", () => {
const workspace = createWorkspace({ withBunLockBinary: true });
try {
const entries = collectTreeEntries({
githubWorkspace: workspace,
manifestPathInput: "manifest.json",
});
const lockEntry = entries.find((entry) => entry.path === "bun.lockb");
assert.ok(lockEntry);
assert.equal(lockEntry.encoding, "base64");
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
});
107 changes: 94 additions & 13 deletions .github/scripts/push-changes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ const fs = require("fs");
const path = require("path");

const MAX_FILE_SIZE = 30 * 1024 * 1024; // 30MB
const OPTIONAL_ROOT_ARTIFACT_FILES = [
"package.json",
"bun.lock",
"bun.lockb",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"npm-shrinkwrap.json",
];

function normalizeBranchName(value) {
const branch = String(value || "")
Expand Down Expand Up @@ -59,7 +68,12 @@ async function getRefSha(octokit, owner, repo, ref) {
});
return result.data.object.sha;
} catch (error) {
if (error && typeof error === "object" && "status" in error && Number(error.status) === 404) {
if (
error &&
typeof error === "object" &&
"status" in error &&
Number(error.status) === 404
) {
return null;
}
throw error;
Expand Down Expand Up @@ -103,7 +117,12 @@ function collectTreeEntries({ githubWorkspace, manifestPathInput }) {
});
}

const workflowComputePath = path.resolve(githubWorkspace, ".github", "workflows", "compute.yml");
const workflowComputePath = path.resolve(
githubWorkspace,
".github",
"workflows",
"compute.yml",
);
if (fs.existsSync(workflowComputePath)) {
treeEntries.push({
path: ".github/workflows/compute.yml",
Expand All @@ -113,14 +132,41 @@ function collectTreeEntries({ githubWorkspace, manifestPathInput }) {
});
}

for (const optionalPath of OPTIONAL_ROOT_ARTIFACT_FILES) {
const fullPath = path.resolve(githubWorkspace, optionalPath);
if (!fs.existsSync(fullPath)) {
continue;
}

if (optionalPath === "bun.lockb") {
treeEntries.push({
path: optionalPath,
mode: "100644",
type: "blob",
content: fs.readFileSync(fullPath).toString("base64"),
encoding: "base64",
});
continue;
}

treeEntries.push({
path: optionalPath,
mode: "100644",
type: "blob",
content: fs.readFileSync(fullPath, "utf8"),
});
}

const distFiles = glob.sync("dist/**/*.{js,cjs,map,json}", {
cwd: githubWorkspace,
absolute: true,
nodir: true,
});

for (const file of distFiles) {
const relativePath = path.relative(githubWorkspace, file).replaceAll("\\", "/");
const relativePath = path
.relative(githubWorkspace, file)
.replaceAll("\\", "/");
const fileStats = fs.statSync(file);
if (fileStats.size > MAX_FILE_SIZE) {
const fileContent = fs.readFileSync(file);
Expand Down Expand Up @@ -181,7 +227,9 @@ async function pushChanges() {
const githubWorkspace = getRequiredEnv("GITHUB_WORKSPACE");
const sourceRef = process.env.SOURCE_REF || process.env.GITHUB_REF_NAME;
if (!sourceRef || !sourceRef.trim()) {
throw new Error("Missing SOURCE_REF or GITHUB_REF_NAME environment variable");
throw new Error(
"Missing SOURCE_REF or GITHUB_REF_NAME environment variable",
);
}
const artifactPrefix = process.env.ARTIFACT_PREFIX || "dist/";

Expand All @@ -197,7 +245,13 @@ async function pushChanges() {
console.log(`Source branch: ${normalizedSourceRef}`);
console.log(`Artifact branch: ${artifactRef}`);

const sourceSha = await resolveSourceSha(octokit, owner, repo, normalizedSourceRef, context.sha);
const sourceSha = await resolveSourceSha(
octokit,
owner,
repo,
normalizedSourceRef,
context.sha,
);
const artifactSha = await getRefSha(octokit, owner, repo, artifactHeadRef);
const parentCommitSha = artifactSha || sourceSha;

Expand All @@ -211,26 +265,51 @@ async function pushChanges() {
githubWorkspace,
manifestPathInput,
});
const includesActionYml = treeEntries.some((entry) => entry.path === "action.yml");
const includesComputeWorkflow = treeEntries.some((entry) => entry.path === ".github/workflows/compute.yml");
const distEntryCount = treeEntries.filter((entry) => entry.path.startsWith("dist/")).length;
const includesActionYml = treeEntries.some(
(entry) => entry.path === "action.yml",
);
const includesComputeWorkflow = treeEntries.some(
(entry) => entry.path === ".github/workflows/compute.yml",
);
const includesPackageJson = treeEntries.some(
(entry) => entry.path === "package.json",
);
const includedLockfiles = OPTIONAL_ROOT_ARTIFACT_FILES.filter(
(file) =>
file !== "package.json" &&
treeEntries.some((entry) => entry.path === file),
);
const distEntryCount = treeEntries.filter((entry) =>
entry.path.startsWith("dist/"),
).length;
console.log(
`Artifact payload entries: manifest.json + ${distEntryCount} dist file(s)${
includesActionYml ? " + action.yml" : ""
}${includesComputeWorkflow ? " + .github/workflows/compute.yml" : ""}`
}${includesComputeWorkflow ? " + .github/workflows/compute.yml" : ""}${includesPackageJson ? " + package.json" : ""}${
includedLockfiles.length ? ` + ${includedLockfiles.join(", ")}` : ""
}`,
);
if (!includesActionYml) {
console.log("Root action.yml not found in workspace; skipping action metadata in artifact payload.");
console.log(
"Root action.yml not found in workspace; skipping action metadata in artifact payload.",
);
}
if (!includesComputeWorkflow) {
console.log("Workflow .github/workflows/compute.yml not found in workspace; skipping workflow metadata in artifact payload.");
console.log(
"Workflow .github/workflows/compute.yml not found in workspace; skipping workflow metadata in artifact payload.",
);
}
if (!includesPackageJson) {
console.log(
"Root package.json not found in workspace; skipping package metadata in artifact payload.",
);
}

const newTreeSha = await createTreeFromEntries(
octokit,
owner,
repo,
treeEntries
treeEntries,
);

if (newTreeSha === parentCommit.data.tree.sha) {
Expand Down Expand Up @@ -263,7 +342,9 @@ async function pushChanges() {
});
}

console.log(`Published artifact commit ${newCommit.data.sha} to ${artifactRef}`);
console.log(
`Published artifact commit ${newCommit.data.sha} to ${artifactRef}`,
);
}

module.exports = {
Expand Down