Source commit: 750f29790fe2046599fbe5b5d06fdb63d92fabff Public tree identity: sha256:02326e08850bb287585789733ea5f3f153e1f7f3fd88afcc24e249107df91506
608 lines
19 KiB
JavaScript
Executable file
608 lines
19 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
const crypto = require("crypto");
|
|
const cp = require("child_process");
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const repo = path.resolve(__dirname, "..");
|
|
const outputRoot = path.resolve(
|
|
process.env.DISASMER_PUBLIC_RELEASE_DIR ||
|
|
path.join(repo, "target/public-release-dryrun")
|
|
);
|
|
const publicTree = path.join(outputRoot, "public-tree");
|
|
const assetsDir = path.join(outputRoot, "assets");
|
|
const stagingDir = path.join(outputRoot, "staging");
|
|
const defaultOperatorEndpoint = "https://disasmer.michelpaulissen.com:9443";
|
|
const forgejoHost = "git.michelpaulissen.com";
|
|
const args = new Set(process.argv.slice(2));
|
|
const includeForgejoWorkflows =
|
|
args.has("--include-forgejo-workflows") ||
|
|
/^(1|true|yes)$/i.test(process.env.DISASMER_INCLUDE_FORGEJO_WORKFLOWS || "");
|
|
const filteredTopLevel = ["private", "experiments", ".git", "target"];
|
|
const publicRepoBranch = process.env.DISASMER_PUBLIC_REPO_BRANCH || "main";
|
|
const publicBinaries = [
|
|
"disasmer",
|
|
"disasmer-coordinator",
|
|
"disasmer-node",
|
|
"disasmer-debug-dap",
|
|
];
|
|
|
|
function commandOutput(command, args, options = {}) {
|
|
try {
|
|
return cp
|
|
.execFileSync(command, args, {
|
|
cwd: repo,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
...options,
|
|
})
|
|
.trim();
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
cp.execFileSync(command, args, {
|
|
cwd: repo,
|
|
stdio: "inherit",
|
|
...options,
|
|
});
|
|
}
|
|
|
|
function ensureDir(dir) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
function filteredOutPatterns() {
|
|
return [
|
|
"private/**",
|
|
"experiments/**",
|
|
".git",
|
|
"target",
|
|
"/*.md",
|
|
...(includeForgejoWorkflows ? [] : [".forgejo/**"]),
|
|
];
|
|
}
|
|
|
|
function isRootMarkdown(relativePath, entry) {
|
|
return (
|
|
!relativePath.includes(path.sep) &&
|
|
(entry.isFile() || entry.isSymbolicLink()) &&
|
|
path.extname(entry.name).toLowerCase() === ".md"
|
|
);
|
|
}
|
|
|
|
function shouldFilter(entry, relativePath) {
|
|
const topLevel = relativePath.split(path.sep)[0];
|
|
if (filteredTopLevel.includes(topLevel)) return true;
|
|
if (topLevel === ".forgejo" && !includeForgejoWorkflows) return true;
|
|
if (isRootMarkdown(relativePath, entry)) return true;
|
|
return false;
|
|
}
|
|
|
|
function copyFilteredTree(src, dest, relative = "") {
|
|
ensureDir(dest);
|
|
const entries = fs
|
|
.readdirSync(src, { withFileTypes: true })
|
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
|
|
for (const entry of entries) {
|
|
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
|
|
if (shouldFilter(entry, childRelative)) {
|
|
continue;
|
|
}
|
|
|
|
const from = path.join(src, entry.name);
|
|
const to = path.join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
copyFilteredTree(from, to, childRelative);
|
|
} else if (entry.isSymbolicLink()) {
|
|
fs.symlinkSync(fs.readlinkSync(from), to);
|
|
} else if (entry.isFile()) {
|
|
fs.copyFileSync(from, to);
|
|
fs.chmodSync(to, fs.statSync(from).mode & 0o777);
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertFilteredTree() {
|
|
for (const excluded of ["private", "experiments"]) {
|
|
if (fs.existsSync(path.join(publicTree, excluded))) {
|
|
throw new Error(`${excluded}/ leaked into the dry-run public tree`);
|
|
}
|
|
}
|
|
if (!includeForgejoWorkflows && fs.existsSync(path.join(publicTree, ".forgejo"))) {
|
|
throw new Error(".forgejo/ leaked into the host-neutral dry-run public tree");
|
|
}
|
|
for (const entry of fs.readdirSync(publicTree, { withFileTypes: true })) {
|
|
if (isRootMarkdown(entry.name, entry)) {
|
|
throw new Error(`root Markdown file leaked into the dry-run public tree: ${entry.name}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function walkFiles(root, relative = "") {
|
|
const dir = path.join(root, relative);
|
|
const entries = fs
|
|
.readdirSync(dir, { withFileTypes: true })
|
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
const childRelative = relative ? path.join(relative, entry.name) : entry.name;
|
|
if (entry.isDirectory()) {
|
|
files.push(...walkFiles(root, childRelative));
|
|
} else if (entry.isFile()) {
|
|
files.push(childRelative);
|
|
} else if (entry.isSymbolicLink()) {
|
|
files.push(childRelative);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
function hashTree(root) {
|
|
const hash = crypto.createHash("sha256");
|
|
for (const file of walkFiles(root)) {
|
|
const absolute = path.join(root, file);
|
|
const stat = fs.lstatSync(absolute);
|
|
hash.update(file.replaceAll(path.sep, "/"));
|
|
hash.update("\0");
|
|
hash.update(String(stat.mode & 0o777));
|
|
hash.update("\0");
|
|
if (stat.isSymbolicLink()) {
|
|
hash.update("symlink");
|
|
hash.update("\0");
|
|
hash.update(fs.readlinkSync(absolute));
|
|
} else {
|
|
hash.update(fs.readFileSync(absolute));
|
|
}
|
|
hash.update("\0");
|
|
}
|
|
return `sha256:${hash.digest("hex")}`;
|
|
}
|
|
|
|
function sha256File(file) {
|
|
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
}
|
|
|
|
function tarGz(output, cwd, inputs) {
|
|
run("tar", ["-czf", output, "-C", cwd, ...inputs]);
|
|
}
|
|
|
|
function platformName() {
|
|
return `${os.platform()}-${os.arch()}`;
|
|
}
|
|
|
|
function binaryName(name) {
|
|
return process.platform === "win32" ? `${name}.exe` : name;
|
|
}
|
|
|
|
function buildPublicBinaries() {
|
|
run("cargo", ["build", "--workspace", "--bins", "--release"], {
|
|
cwd: publicTree,
|
|
});
|
|
}
|
|
|
|
function stageBinaryAssets(releaseName) {
|
|
const stageRoot = path.join(stagingDir, "binaries");
|
|
const binDir = path.join(stageRoot, "bin");
|
|
fs.rmSync(stageRoot, { recursive: true, force: true });
|
|
ensureDir(binDir);
|
|
|
|
for (const binary of publicBinaries) {
|
|
const fileName = binaryName(binary);
|
|
const built = path.join(publicTree, "target", "release", fileName);
|
|
if (!fs.existsSync(built)) {
|
|
throw new Error(`expected release binary ${built}`);
|
|
}
|
|
const staged = path.join(binDir, fileName);
|
|
fs.copyFileSync(built, staged);
|
|
fs.chmodSync(staged, 0o755);
|
|
}
|
|
|
|
const archive = path.join(
|
|
assetsDir,
|
|
`disasmer-public-binaries-${releaseName}-${platformName()}.tar.gz`
|
|
);
|
|
tarGz(archive, stageRoot, ["."]);
|
|
return archive;
|
|
}
|
|
|
|
function stageSourceAsset(releaseName) {
|
|
const archive = path.join(assetsDir, `disasmer-public-source-${releaseName}.tar.gz`);
|
|
tarGz(archive, publicTree, ["."]);
|
|
return archive;
|
|
}
|
|
|
|
function stageExtensionAsset() {
|
|
const packageJson = JSON.parse(
|
|
fs.readFileSync(path.join(publicTree, "vscode-extension/package.json"), "utf8")
|
|
);
|
|
const archive = path.join(
|
|
assetsDir,
|
|
`${packageJson.name}-${packageJson.version}.vsix`
|
|
);
|
|
run(
|
|
"npx",
|
|
[
|
|
"--yes",
|
|
"@vscode/vsce",
|
|
"package",
|
|
"--allow-missing-repository",
|
|
"--out",
|
|
archive,
|
|
],
|
|
{ cwd: path.join(publicTree, "vscode-extension") }
|
|
);
|
|
return archive;
|
|
}
|
|
|
|
function resolverInstructions() {
|
|
if (process.env.DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS) {
|
|
return process.env.DISASMER_PUBLIC_DRYRUN_RESOLVER_INSTRUCTIONS.trim();
|
|
}
|
|
if (process.env.DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY) {
|
|
return [
|
|
"Add the controlled hosts entry supplied for this dry run:",
|
|
"",
|
|
"```",
|
|
process.env.DISASMER_PUBLIC_DRYRUN_HOSTS_ENTRY.trim(),
|
|
"```",
|
|
].join("\n");
|
|
}
|
|
if (process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_IP) {
|
|
return [
|
|
"Add this controlled hosts entry for the dry run:",
|
|
"",
|
|
"```",
|
|
`${process.env.DISASMER_PUBLIC_RELEASE_DRYRUN_IP} disasmer.michelpaulissen.com`,
|
|
"```",
|
|
].join("\n");
|
|
}
|
|
return [
|
|
"`disasmer.michelpaulissen.com` should resolve through public DNS. If it",
|
|
"does not resolve yet, wait for DNS propagation or use the fallback hosts",
|
|
"entry supplied with the invitation.",
|
|
].join("\n");
|
|
}
|
|
|
|
function writeGettingStartedAsset(releaseName, publicTreeIdentity, resolution) {
|
|
const file = path.join(assetsDir, `DISASMER_PUBLIC_DRYRUN_GETTING_STARTED-${releaseName}.md`);
|
|
fs.writeFileSync(
|
|
file,
|
|
`# Disasmer Public Dry Run
|
|
|
|
This dry run is a real Disasmer deployment for selected external users. If
|
|
public DNS has not propagated yet, use the fallback resolution instructions
|
|
below.
|
|
|
|
The default operator is the private hosted coordinator at
|
|
\`https://disasmer.michelpaulissen.com:9443\`. The word \`public\` in this dry run
|
|
refers to the public Forgejo repository, release downloads, selected-user
|
|
network access, and public client protocol compatibility. It does not mean the
|
|
server is the standalone open-source/public coordinator.
|
|
|
|
## DNS
|
|
|
|
${resolution}
|
|
|
|
## Install
|
|
|
|
1. Download the binary archive for your platform from the Forgejo Release at
|
|
\`git.michelpaulissen.com\`.
|
|
2. Verify it against \`SHA256SUMS\`.
|
|
3. Extract the archive and put the \`bin/\` directory on your \`PATH\`.
|
|
4. Download \`disasmer-vscode-*.vsix\` if you want the debugger and Disasmer
|
|
side views, then install it:
|
|
|
|
\`\`\`bash
|
|
code --install-extension disasmer-vscode-*.vsix
|
|
\`\`\`
|
|
|
|
## Connect
|
|
|
|
Use the default operator endpoint:
|
|
|
|
\`\`\`bash
|
|
disasmer login --browser
|
|
disasmer bundle inspect --project examples/launch-build-demo
|
|
\`\`\`
|
|
|
|
\`disasmer login --browser\` opens your browser through the barebones
|
|
\`https://disasmer.michelpaulissen.com\` login site and returns to the CLI
|
|
through a local callback.
|
|
|
|
Enroll a node identity with the grant supplied by the invitation:
|
|
|
|
\`\`\`bash
|
|
disasmer node attach --coordinator https://disasmer.michelpaulissen.com:9443 --enrollment-grant <grant> --public-key <public-key>
|
|
\`\`\`
|
|
|
|
That command proves the identity/enrollment path and then exits. To actually run
|
|
work for the flagship workflow, leave a worker process running in another
|
|
terminal. Use a fresh enrollment grant, or skip the attach command above and use
|
|
the worker command directly:
|
|
|
|
\`\`\`bash
|
|
disasmer-node --coordinator https://disasmer.michelpaulissen.com:9443 --tenant <tenant> --project-id <project> --node <node-id> --public-key <public-key> --enrollment-grant <grant> --worker --emit-ready
|
|
\`\`\`
|
|
|
|
Then run the flagship workflow from the filtered public repository while that
|
|
worker is still running:
|
|
|
|
\`\`\`bash
|
|
disasmer run --project examples/launch-build-demo build
|
|
\`\`\`
|
|
|
|
The dry-run public tree identity is \`${publicTreeIdentity}\`.
|
|
The release name is \`${releaseName}\`.
|
|
`,
|
|
"utf8"
|
|
);
|
|
return file;
|
|
}
|
|
|
|
function writeInviteAsset(releaseName, publicTreeIdentity, resolution) {
|
|
const file = path.join(assetsDir, `DISASMER_PUBLIC_DRYRUN_INVITE-${releaseName}.md`);
|
|
const publicRepo =
|
|
process.env.DISASMER_PUBLIC_REPO_URL ||
|
|
"https://git.michelpaulissen.com/<owner>/<public-repo>";
|
|
const releaseUrl =
|
|
process.env.DISASMER_FORGEJO_RELEASE_URL ||
|
|
"the Forgejo Release attached to the public repository";
|
|
fs.writeFileSync(
|
|
file,
|
|
`# Disasmer Public Dry Run Invite
|
|
|
|
This invite is for selected external users, such as friends helping test the
|
|
MVP release experience. The deployment is real and externally reachable, but it
|
|
is intentionally not broadly advertised yet.
|
|
|
|
The default operator behind this invite is the private hosted coordinator at
|
|
\`https://disasmer.michelpaulissen.com:9443\`. The public part is the Forgejo
|
|
repository, release downloads, network-reachable dry run, and public client
|
|
protocol used by the binaries; the hosted server is not the standalone public
|
|
coordinator.
|
|
|
|
If public DNS has not propagated yet, make sure \`disasmer.michelpaulissen.com\`
|
|
resolves to the deployment host before running the CLI.
|
|
|
|
## Links
|
|
|
|
- Public repository: ${publicRepo}
|
|
- Release downloads: ${releaseUrl}
|
|
- Default operator endpoint: ${defaultOperatorEndpoint}
|
|
- Public tree identity: ${publicTreeIdentity}
|
|
- Release name: ${releaseName}
|
|
|
|
## DNS
|
|
|
|
${resolution}
|
|
|
|
## First run
|
|
|
|
1. Download the binary archive for your platform and \`SHA256SUMS\` from the
|
|
Forgejo Release.
|
|
2. Extract the archive and put \`bin/\` on your \`PATH\`.
|
|
3. Optionally install \`disasmer-vscode-*.vsix\` with
|
|
\`code --install-extension disasmer-vscode-*.vsix\`.
|
|
4. Run \`disasmer login --browser\`; it opens the barebones
|
|
\`https://disasmer.michelpaulissen.com\` login site and returns to the CLI
|
|
through a local callback.
|
|
5. Enroll a node identity with the enrollment grant supplied out of band.
|
|
6. Start a long-lived worker with a fresh enrollment grant, or use the worker
|
|
command directly instead of step 5:
|
|
\`disasmer-node --coordinator https://disasmer.michelpaulissen.com:9443 --tenant <tenant> --project-id <project> --node <node-id> --public-key <public-key> --enrollment-grant <grant> --worker --emit-ready\`.
|
|
7. Run \`disasmer run --project examples/launch-build-demo build\` from the
|
|
public repository checkout while the worker process is still running.
|
|
`,
|
|
"utf8"
|
|
);
|
|
return file;
|
|
}
|
|
|
|
function writeSha256Sums(assets) {
|
|
const sumsPath = path.join(assetsDir, "SHA256SUMS");
|
|
const lines = assets
|
|
.map((asset) => `${sha256File(asset)} ${path.basename(asset)}`)
|
|
.sort()
|
|
.join("\n");
|
|
fs.writeFileSync(sumsPath, `${lines}\n`);
|
|
return sumsPath;
|
|
}
|
|
|
|
function boolEnv(name) {
|
|
return /^(1|true|yes)$/i.test(process.env[name] || "");
|
|
}
|
|
|
|
function writePublicTreeProvenance(sourceCommit, releaseName) {
|
|
const provenance = {
|
|
kind: "disasmer-filtered-public-tree",
|
|
source_commit: sourceCommit,
|
|
release_name: releaseName,
|
|
filtered_out: filteredOutPatterns(),
|
|
public_export: {
|
|
host_neutral: !includeForgejoWorkflows,
|
|
include_forgejo_workflows: includeForgejoWorkflows,
|
|
},
|
|
forgejo_host: forgejoHost,
|
|
default_operator_endpoint: defaultOperatorEndpoint,
|
|
};
|
|
fs.writeFileSync(
|
|
path.join(publicTree, "DISASMER_PUBLIC_TREE.json"),
|
|
`${JSON.stringify(provenance, null, 2)}\n`
|
|
);
|
|
}
|
|
|
|
function publishPublicTree(releaseName, sourceCommit, publicTreeIdentity) {
|
|
const remote =
|
|
process.env.DISASMER_PUBLIC_REPO_REMOTE ||
|
|
process.env.DISASMER_PUBLIC_REPO_URL ||
|
|
null;
|
|
const enabled = boolEnv("DISASMER_PUBLISH_PUBLIC_TREE");
|
|
const result = {
|
|
enabled,
|
|
remote,
|
|
branch: publicRepoBranch,
|
|
commit: null,
|
|
pushed: false,
|
|
};
|
|
|
|
if (!enabled) {
|
|
return result;
|
|
}
|
|
if (!remote) {
|
|
throw new Error(
|
|
"DISASMER_PUBLISH_PUBLIC_TREE requires DISASMER_PUBLIC_REPO_REMOTE or DISASMER_PUBLIC_REPO_URL"
|
|
);
|
|
}
|
|
if (!remote.includes(forgejoHost)) {
|
|
throw new Error(`public repo remote must point at ${forgejoHost}: ${remote}`);
|
|
}
|
|
|
|
run("git", ["init"], { cwd: publicTree });
|
|
run("git", ["checkout", "-B", publicRepoBranch], { cwd: publicTree });
|
|
run("git", ["config", "user.name", "Disasmer release dry run"], {
|
|
cwd: publicTree,
|
|
});
|
|
run("git", ["config", "user.email", "release-dryrun@disasmer.invalid"], {
|
|
cwd: publicTree,
|
|
});
|
|
run("git", ["add", "."], { cwd: publicTree });
|
|
run(
|
|
"git",
|
|
[
|
|
"commit",
|
|
"-m",
|
|
`Public dry run ${releaseName}`,
|
|
"-m",
|
|
`Source commit: ${sourceCommit}`,
|
|
"-m",
|
|
`Public tree identity: ${publicTreeIdentity}`,
|
|
],
|
|
{ cwd: publicTree }
|
|
);
|
|
result.commit = commandOutput("git", ["rev-parse", "HEAD"], { cwd: publicTree });
|
|
run("git", ["remote", "add", "public", remote], { cwd: publicTree });
|
|
const pushArgs = ["push", "public", `HEAD:${publicRepoBranch}`];
|
|
if (boolEnv("DISASMER_PUBLIC_REPO_PUSH_FORCE_WITH_LEASE")) {
|
|
run(
|
|
"git",
|
|
[
|
|
"fetch",
|
|
"public",
|
|
`refs/heads/${publicRepoBranch}:refs/remotes/public/${publicRepoBranch}`,
|
|
],
|
|
{ cwd: publicTree }
|
|
);
|
|
pushArgs.splice(1, 0, "--force-with-lease");
|
|
}
|
|
run("git", pushArgs, { cwd: publicTree });
|
|
result.pushed = true;
|
|
return result;
|
|
}
|
|
|
|
function main() {
|
|
const sourceCommit =
|
|
process.env.DISASMER_ACCEPTANCE_COMMIT ||
|
|
commandOutput("git", ["rev-parse", "HEAD"]) ||
|
|
"unknown";
|
|
const shortCommit = sourceCommit === "unknown" ? "unknown" : sourceCommit.slice(0, 12);
|
|
const releaseName = process.env.DISASMER_PUBLIC_RELEASE_NAME || `dryrun-${shortCommit}`;
|
|
const sourceStatus = commandOutput("git", ["status", "--short"]);
|
|
const sourceTreeClean = sourceStatus === null ? null : sourceStatus === "";
|
|
|
|
fs.rmSync(outputRoot, { recursive: true, force: true });
|
|
ensureDir(publicTree);
|
|
ensureDir(assetsDir);
|
|
ensureDir(stagingDir);
|
|
|
|
copyFilteredTree(repo, publicTree);
|
|
assertFilteredTree();
|
|
writePublicTreeProvenance(sourceCommit, releaseName);
|
|
const publicTreeIdentity = hashTree(publicTree);
|
|
const sourceArchive = stageSourceAsset(releaseName);
|
|
|
|
run("node", ["scripts/public-release-dryrun-contract-smoke.js"], {
|
|
cwd: publicTree,
|
|
});
|
|
const publicTreePublish = publishPublicTree(
|
|
releaseName,
|
|
sourceCommit,
|
|
publicTreeIdentity
|
|
);
|
|
buildPublicBinaries();
|
|
const binaryArchive = stageBinaryAssets(releaseName);
|
|
const extensionArchive = stageExtensionAsset();
|
|
const resolution = resolverInstructions();
|
|
const gettingStarted = writeGettingStartedAsset(
|
|
releaseName,
|
|
publicTreeIdentity,
|
|
resolution
|
|
);
|
|
const invite = writeInviteAsset(releaseName, publicTreeIdentity, resolution);
|
|
const assets = [
|
|
sourceArchive,
|
|
binaryArchive,
|
|
extensionArchive,
|
|
gettingStarted,
|
|
invite,
|
|
];
|
|
const sha256Sums = writeSha256Sums(assets);
|
|
|
|
const manifest = {
|
|
kind: "disasmer-public-release-dryrun",
|
|
release_name: releaseName,
|
|
source_commit: sourceCommit,
|
|
source_tree_clean: sourceTreeClean,
|
|
public_tree_identity: publicTreeIdentity,
|
|
public_tree: publicTree,
|
|
filtered_out: filteredOutPatterns(),
|
|
public_export: {
|
|
host_neutral: !includeForgejoWorkflows,
|
|
include_forgejo_workflows: includeForgejoWorkflows,
|
|
},
|
|
forgejo_host: forgejoHost,
|
|
public_repo_url: process.env.DISASMER_PUBLIC_REPO_URL || publicTreePublish.remote,
|
|
public_repo_remote: process.env.DISASMER_PUBLIC_REPO_REMOTE || null,
|
|
public_tree_publish: publicTreePublish,
|
|
forgejo_release_url: process.env.DISASMER_FORGEJO_RELEASE_URL || null,
|
|
default_operator_endpoint: defaultOperatorEndpoint,
|
|
dns_publication_state:
|
|
process.env.DISASMER_DNS_PUBLICATION_STATE || "published",
|
|
resolver_override:
|
|
process.env.DISASMER_RESOLVER_OVERRIDE || "none-required-public-dns",
|
|
platform: platformName(),
|
|
tool_versions: {
|
|
node: process.version,
|
|
rustc: commandOutput("rustc", ["--version"]) || null,
|
|
cargo: commandOutput("cargo", ["--version"]) || null,
|
|
tar: commandOutput("tar", ["--version"]) || null,
|
|
},
|
|
commands: [
|
|
"node scripts/public-release-dryrun-contract-smoke.js",
|
|
...(publicTreePublish.enabled
|
|
? [`git push public HEAD:${publicRepoBranch}`]
|
|
: []),
|
|
"cargo build --workspace --bins --release",
|
|
],
|
|
assets: [...assets, sha256Sums].map((asset) => ({
|
|
file: asset,
|
|
name: path.basename(asset),
|
|
sha256: sha256File(asset),
|
|
})),
|
|
notes: [
|
|
"Upload the assets to the Forgejo Release for the filtered public repository.",
|
|
"The real service deployment and full e2e dry run remain separate acceptance evidence.",
|
|
],
|
|
};
|
|
|
|
const manifestPath = path.join(outputRoot, "public-release-manifest.json");
|
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
console.log(JSON.stringify({ manifest: manifestPath, assets: assetsDir }, null, 2));
|
|
}
|
|
|
|
main();
|