From 987beaabe2ede2a60ead18cc4e9814e6fbafb695 Mon Sep 17 00:00:00 2001 From: gentlementlegen Date: Tue, 10 Mar 2026 01:47:27 +0900 Subject: [PATCH] fix(artifact): include package and lock files in dist payload --- .../scripts/__tests__/push-changes.test.js | 99 ++++++++++++++-- .github/scripts/push-changes.js | 107 +++++++++++++++--- 2 files changed, 184 insertions(+), 22 deletions(-) diff --git a/.github/scripts/__tests__/push-changes.test.js b/.github/scripts/__tests__/push-changes.test.js index c74e664..3cd00ee 100644 --- a/.github/scripts/__tests__/push-changes.test.js +++ b/.github/scripts/__tests__/push-changes.test.js @@ -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"); }); @@ -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; } @@ -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 }); } @@ -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 }); + } + }); }); diff --git a/.github/scripts/push-changes.js b/.github/scripts/push-changes.js index 12f286a..0717981 100644 --- a/.github/scripts/push-changes.js +++ b/.github/scripts/push-changes.js @@ -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 || "") @@ -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; @@ -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", @@ -113,6 +132,31 @@ 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, @@ -120,7 +164,9 @@ function collectTreeEntries({ githubWorkspace, manifestPathInput }) { }); 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); @@ -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/"; @@ -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; @@ -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) { @@ -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 = {