refactor(release): use shared lib in release-boss + load env at startup
Boss script previously did not source .env.release / .env.release.local, which silently dropped any release-only environment variables that the remote workflow dispatch and asset download paths may rely on. Match release-public-cloudflare's behavior by calling loadEnvFile for both files before any gh / wrangler invocation.
This commit is contained in:
1 parent
18bcd0abfc
commit
e54aea5942
1 file changed
+40
-207
+40
-207
@@ -1,9 +1,11 @@
|
|||||||
import { spawn } from "node:child_process";
|
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdtemp, readFile, mkdir, cp, readdir } from "node:fs/promises";
|
import { readFile, mkdir, cp, readdir } from "node:fs/promises";
|
||||||
import os from "node:os";
|
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { loadEnvFile } from "./release/lib/dotenv.mjs";
|
||||||
|
import { createRunner } from "./release/lib/process.mjs";
|
||||||
|
import { createWorkflowHelpers } from "./release/lib/workflow.mjs";
|
||||||
|
import { createSnapshotHelpers } from "./release/lib/snapshot.mjs";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const rootDir = join(__dirname, "..");
|
const rootDir = join(__dirname, "..");
|
||||||
@@ -42,6 +44,22 @@ const bossSnapshotIncludes = [
|
|||||||
"boss-build/electron-builder-boss.yml",
|
"boss-build/electron-builder-boss.yml",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const run = createRunner(rootDir);
|
||||||
|
const {
|
||||||
|
getGitHubHttpAuthHeader,
|
||||||
|
getRepoInfo,
|
||||||
|
assertRemoteWorkflowExists,
|
||||||
|
waitForWorkflowRun,
|
||||||
|
waitForWorkflowCompletion,
|
||||||
|
downloadRemoteAssets,
|
||||||
|
cleanupRemoteTransportRelease,
|
||||||
|
} = createWorkflowHelpers(run);
|
||||||
|
const {
|
||||||
|
createSnapshotBranch,
|
||||||
|
cleanupRemoteBranch,
|
||||||
|
cleanupLocalWorktree,
|
||||||
|
} = createSnapshotHelpers(run, getGitHubHttpAuthHeader);
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const options = {
|
const options = {
|
||||||
version: null,
|
version: null,
|
||||||
@@ -83,89 +101,6 @@ Options:
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(command, args, options = {}) {
|
|
||||||
const {
|
|
||||||
cwd = rootDir,
|
|
||||||
env = process.env,
|
|
||||||
input = null,
|
|
||||||
capture = false,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const child = spawn(command, args, {
|
|
||||||
cwd,
|
|
||||||
env,
|
|
||||||
stdio: capture ? ["pipe", "pipe", "pipe"] : ["pipe", "inherit", "inherit"],
|
|
||||||
shell: process.platform === "win32",
|
|
||||||
});
|
|
||||||
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
if (capture) {
|
|
||||||
child.stdout.on("data", (chunk) => {
|
|
||||||
stdout += chunk.toString();
|
|
||||||
});
|
|
||||||
child.stderr.on("data", (chunk) => {
|
|
||||||
stderr += chunk.toString();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input) {
|
|
||||||
child.stdin.write(input);
|
|
||||||
}
|
|
||||||
child.stdin.end();
|
|
||||||
|
|
||||||
child.on("error", reject);
|
|
||||||
child.on("close", (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve({ stdout, stderr });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject(new Error(stderr || stdout || `${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ghJson(args) {
|
|
||||||
const { stdout } = await run("gh", args, { capture: true });
|
|
||||||
return JSON.parse(stdout);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getGitHubHttpAuthHeader() {
|
|
||||||
const tokenResult = await run("gh", ["auth", "token"], { capture: true });
|
|
||||||
const token = tokenResult.stdout.trim();
|
|
||||||
if (!token) {
|
|
||||||
throw new Error("Unable to read a GitHub token from gh auth.");
|
|
||||||
}
|
|
||||||
return Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRepoInfo() {
|
|
||||||
const info = await ghJson(["repo", "view", "--json", "nameWithOwner,defaultBranchRef"]);
|
|
||||||
const defaultBranch = info.defaultBranchRef?.name;
|
|
||||||
if (!info.nameWithOwner || !defaultBranch) {
|
|
||||||
throw new Error("Unable to resolve GitHub repository name/default branch from gh.");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
nameWithOwner: info.nameWithOwner,
|
|
||||||
defaultBranch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertRemoteBossWorkflowExists(repo) {
|
|
||||||
try {
|
|
||||||
await run("gh", [
|
|
||||||
"api",
|
|
||||||
`repos/${repo.nameWithOwner}/contents/${remoteBossWorkflowFile}?ref=${repo.defaultBranch}`,
|
|
||||||
], { capture: true });
|
|
||||||
} catch {
|
|
||||||
throw new Error(
|
|
||||||
`Remote workflow ${remoteBossWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
|
||||||
"Commit and push this Boss automation workflow before using scripts/release-boss.mjs.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readPackageJson() {
|
async function readPackageJson() {
|
||||||
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
||||||
}
|
}
|
||||||
@@ -188,41 +123,17 @@ async function ensureRemoteBuildPrerequisites(repo) {
|
|||||||
if (!existsSync(join(rootDir, remoteBossWorkflowFile))) {
|
if (!existsSync(join(rootDir, remoteBossWorkflowFile))) {
|
||||||
throw new Error(`Required workflow file missing locally: ${remoteBossWorkflowFile}`);
|
throw new Error(`Required workflow file missing locally: ${remoteBossWorkflowFile}`);
|
||||||
}
|
}
|
||||||
await assertRemoteBossWorkflowExists(repo);
|
await assertRemoteWorkflowExists(
|
||||||
|
repo,
|
||||||
|
remoteBossWorkflowFile,
|
||||||
|
`Remote workflow ${remoteBossWorkflowFile} was not found on ${repo.defaultBranch}. ` +
|
||||||
|
"Commit and push this Boss automation workflow before using scripts/release-boss.mjs.",
|
||||||
|
);
|
||||||
await run("gh", ["auth", "status"], { capture: true });
|
await run("gh", ["auth", "status"], { capture: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createSnapshotBranch(repo, branchName, version) {
|
function buildSnapshotCommitMessage(version) {
|
||||||
const tempDir = await mkdtemp(join(os.tmpdir(), "agentcompany-release-"));
|
return [
|
||||||
const auth = await getGitHubHttpAuthHeader();
|
|
||||||
await run("git", [
|
|
||||||
"-c",
|
|
||||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
|
||||||
"fetch",
|
|
||||||
`https://github.com/${repo.nameWithOwner}.git`,
|
|
||||||
repo.defaultBranch,
|
|
||||||
]);
|
|
||||||
await run("git", ["worktree", "add", "--detach", tempDir, "FETCH_HEAD"]);
|
|
||||||
await run("git", ["-C", tempDir, "checkout", "-b", branchName]);
|
|
||||||
|
|
||||||
const rsyncArgs = ["-a", "--delete"];
|
|
||||||
for (const pattern of syncExcludes) {
|
|
||||||
rsyncArgs.push("--exclude", pattern);
|
|
||||||
}
|
|
||||||
rsyncArgs.push(`${rootDir}/`, `${tempDir}/`);
|
|
||||||
await run("rsync", rsyncArgs);
|
|
||||||
|
|
||||||
await run("git", ["-C", tempDir, "add", "-A"]);
|
|
||||||
await forceStageBossSnapshotFiles(tempDir);
|
|
||||||
const diffStatus = await run("git", ["-C", tempDir, "diff", "--cached", "--quiet"], { capture: true })
|
|
||||||
.then(() => false)
|
|
||||||
.catch(() => true);
|
|
||||||
|
|
||||||
if (!diffStatus) {
|
|
||||||
throw new Error("Snapshot branch has no staged changes after syncing the workspace.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const commitMessage = [
|
|
||||||
`Package Boss build ${version} from the current workspace snapshot`,
|
`Package Boss build ${version} from the current workspace snapshot`,
|
||||||
"",
|
"",
|
||||||
"Constraint: Boss Windows artifacts are built on a native GitHub runner from a temporary snapshot branch",
|
"Constraint: Boss Windows artifacts are built on a native GitHub runner from a temporary snapshot branch",
|
||||||
@@ -233,47 +144,6 @@ async function createSnapshotBranch(repo, branchName, version) {
|
|||||||
"Tested: local Boss mac build stages before snapshot creation",
|
"Tested: local Boss mac build stages before snapshot creation",
|
||||||
"Not-tested: remote Boss Windows packaging before workflow execution",
|
"Not-tested: remote Boss Windows packaging before workflow execution",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
await run("git", ["-C", tempDir, "commit", "-m", commitMessage]);
|
|
||||||
|
|
||||||
await run(
|
|
||||||
"git",
|
|
||||||
[
|
|
||||||
"-C",
|
|
||||||
tempDir,
|
|
||||||
"-c",
|
|
||||||
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${auth}`,
|
|
||||||
"push",
|
|
||||||
`https://github.com/${repo.nameWithOwner}.git`,
|
|
||||||
branchName,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return tempDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForWorkflowRun(repo, workflowName, branchName, timeoutMs) {
|
|
||||||
const startedAt = Date.now();
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
|
||||||
const runs = await ghJson([
|
|
||||||
"run",
|
|
||||||
"list",
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"--workflow",
|
|
||||||
workflowName,
|
|
||||||
"--branch",
|
|
||||||
branchName,
|
|
||||||
"--limit",
|
|
||||||
"1",
|
|
||||||
"--json",
|
|
||||||
"databaseId,status,conclusion,url,displayTitle",
|
|
||||||
]);
|
|
||||||
if (runs.length > 0) {
|
|
||||||
return runs[0];
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
||||||
}
|
|
||||||
throw new Error(`Timed out waiting for workflow ${workflowName} on branch ${branchName}.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dispatchRemoteBossWorkflow(repo, branchName, version, releaseTag, releaseTitle) {
|
async function dispatchRemoteBossWorkflow(repo, branchName, version, releaseTag, releaseTitle) {
|
||||||
@@ -294,40 +164,6 @@ async function dispatchRemoteBossWorkflow(repo, branchName, version, releaseTag,
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitForWorkflowCompletion(repo, runId, timeoutMs) {
|
|
||||||
const startedAt = Date.now();
|
|
||||||
while (Date.now() - startedAt < timeoutMs) {
|
|
||||||
const runInfo = await ghJson([
|
|
||||||
"run",
|
|
||||||
"view",
|
|
||||||
String(runId),
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"--json",
|
|
||||||
"status,conclusion,url",
|
|
||||||
]);
|
|
||||||
if (runInfo.status === "completed") {
|
|
||||||
return runInfo;
|
|
||||||
}
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
|
||||||
}
|
|
||||||
throw new Error(`Timed out waiting for workflow run ${runId} to complete.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadRemoteAssets(repo, releaseTag) {
|
|
||||||
const downloadRoot = await mkdtemp(join(os.tmpdir(), "agentcompany-transport-"));
|
|
||||||
await run("gh", [
|
|
||||||
"release",
|
|
||||||
"download",
|
|
||||||
releaseTag,
|
|
||||||
"--repo",
|
|
||||||
repo.nameWithOwner,
|
|
||||||
"-D",
|
|
||||||
downloadRoot,
|
|
||||||
]);
|
|
||||||
return downloadRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stageBossRemoteAssets(downloadRoot) {
|
async function stageBossRemoteAssets(downloadRoot) {
|
||||||
await mkdir(bossOutDir, { recursive: true });
|
await mkdir(bossOutDir, { recursive: true });
|
||||||
const files = await readdir(downloadRoot);
|
const files = await readdir(downloadRoot);
|
||||||
@@ -342,21 +178,11 @@ async function buildBossMac(version) {
|
|||||||
await run("node", ["boss-build/build-boss-edition.mjs", `--platform=mac`, `--version=${version}`]);
|
await run("node", ["boss-build/build-boss-edition.mjs", `--platform=mac`, `--version=${version}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupRemoteTransportRelease(repo, releaseTag) {
|
|
||||||
await run("gh", ["release", "delete", releaseTag, "--repo", repo.nameWithOwner, "--yes"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupRemoteBranch(repo, branchName) {
|
|
||||||
await run("gh", ["api", "-X", "DELETE", `repos/${repo.nameWithOwner}/git/refs/heads/${branchName}`]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function cleanupLocalWorktree(tempDir) {
|
|
||||||
await run("git", ["worktree", "remove", "--force", tempDir]);
|
|
||||||
await run("git", ["worktree", "prune"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const options = parseArgs(process.argv.slice(2));
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
await loadEnvFile(join(rootDir, ".env.release.local"));
|
||||||
|
await loadEnvFile(join(rootDir, ".env.release"));
|
||||||
|
|
||||||
const repo = await getRepoInfo();
|
const repo = await getRepoInfo();
|
||||||
await ensureRemoteBuildPrerequisites(repo);
|
await ensureRemoteBuildPrerequisites(repo);
|
||||||
|
|
||||||
@@ -371,7 +197,14 @@ async function main() {
|
|||||||
const branchName = `boss-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
const branchName = `boss-build-${nextVersion.replace(/\./g, "-")}-${timestamp}`;
|
||||||
const releaseTitle = `Boss Release ${nextVersion}`;
|
const releaseTitle = `Boss Release ${nextVersion}`;
|
||||||
|
|
||||||
const tempDir = await createSnapshotBranch(repo, branchName, nextVersion);
|
const tempDir = await createSnapshotBranch({
|
||||||
|
repo,
|
||||||
|
branchName,
|
||||||
|
rootDir,
|
||||||
|
syncExcludes,
|
||||||
|
commitMessage: buildSnapshotCommitMessage(nextVersion),
|
||||||
|
afterRsync: forceStageBossSnapshotFiles,
|
||||||
|
});
|
||||||
let cleanupPending = true;
|
let cleanupPending = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user