feat: migrate todo note to nc01 github storage
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
release_dir=${UNIDESK_HOST_RELEASE_DIR:-/workspace/release}
|
||||
source_dir=${UNIDESK_HOST_SOURCE_DIR:-/workspace/source}
|
||||
digest_file=${UNIDESK_HOST_DIGEST_FILE:-/workspace/image-digest-ref}
|
||||
metadata_file=${UNIDESK_HOST_BUILD_METADATA_FILE:-/workspace/build-metadata.json}
|
||||
build_log=${UNIDESK_HOST_BUILD_LOG:-/workspace/image-build.log}
|
||||
source_commit=${SOURCE_COMMIT:?SOURCE_COMMIT is required}
|
||||
|
||||
enabled=$(cat "$release_dir/enabled")
|
||||
if [ "$enabled" != true ]; then
|
||||
: >"$digest_file"
|
||||
: >"$metadata_file"
|
||||
printf '{"ok":true,"phase":"image-build","status":"skipped","reason":"delivery-disabled","sourceCommit":"%s","valuesPrinted":false}\n' "$source_commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
image_repository=$(cat "$release_dir/image-repository")
|
||||
dockerfile=$(cat "$release_dir/dockerfile")
|
||||
network_mode=$(cat "$release_dir/network-mode")
|
||||
http_proxy=$(cat "$release_dir/http-proxy")
|
||||
https_proxy=$(cat "$release_dir/https-proxy")
|
||||
all_proxy=$(cat "$release_dir/all-proxy")
|
||||
no_proxy=$(cat "$release_dir/no-proxy")
|
||||
image_ref="$image_repository:$(printf '%s' "$source_commit" | cut -c1-12)"
|
||||
|
||||
rm -f "$metadata_file" "$build_log" "$digest_file"
|
||||
env HTTP_PROXY="$http_proxy" HTTPS_PROXY="$https_proxy" ALL_PROXY="$all_proxy" NO_PROXY="$no_proxy" \
|
||||
http_proxy="$http_proxy" https_proxy="$https_proxy" all_proxy="$all_proxy" no_proxy="$no_proxy" \
|
||||
buildctl-daemonless.sh build \
|
||||
--allow network.host \
|
||||
--frontend dockerfile.v0 \
|
||||
--local context="$source_dir" \
|
||||
--local dockerfile="$source_dir" \
|
||||
--opt "filename=$dockerfile" \
|
||||
--opt "network=$network_mode" \
|
||||
--opt "build-arg:HTTP_PROXY=$http_proxy" \
|
||||
--opt "build-arg:HTTPS_PROXY=$https_proxy" \
|
||||
--opt "build-arg:ALL_PROXY=$all_proxy" \
|
||||
--opt "build-arg:NO_PROXY=$no_proxy" \
|
||||
--metadata-file "$metadata_file" \
|
||||
--output "type=image,name=$image_ref,push=true,registry.insecure=true" >"$build_log" 2>&1
|
||||
cat "$build_log"
|
||||
test -s "$metadata_file"
|
||||
printf '%s\n' "$image_ref" >"$digest_file"
|
||||
printf '{"ok":true,"phase":"image-build","status":"built","sourceCommit":"%s","imageRef":"%s","metadataReady":true,"valuesPrinted":false}\n' "$source_commit" "$image_ref"
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
function option(name) {
|
||||
const index = process.argv.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
const value = process.argv[index + 1];
|
||||
if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function record(value, path) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function required(value, path) {
|
||||
if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${path} must be a non-empty single-line string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function valueAt(root, reference) {
|
||||
return reference.split(".").reduce((value, key) => record(value, reference)[key], root);
|
||||
}
|
||||
|
||||
function writeValue(outputDir, name, value) {
|
||||
writeFileSync(resolve(outputDir, name), `${value}\n`, { encoding: "utf8", mode: 0o644 });
|
||||
}
|
||||
|
||||
function main() {
|
||||
const configPath = option("--config") ?? "config/unidesk-host-k8s.yaml";
|
||||
const outputDir = resolve(required(option("--output-dir"), "--output-dir"));
|
||||
const config = record(Bun.YAML.parse(readFileSync(configPath, "utf8")), configPath);
|
||||
const delivery = record(config.delivery, "delivery");
|
||||
const serviceRef = required(delivery.serviceRef, "delivery.serviceRef");
|
||||
const service = record(valueAt(config, serviceRef), serviceRef);
|
||||
const build = record(delivery.build, "delivery.build");
|
||||
const proxy = record(build.proxy, "delivery.build.proxy");
|
||||
const image = record(delivery.image, "delivery.image");
|
||||
const enabled = delivery.enabled === true;
|
||||
if (delivery.enabled !== true && delivery.enabled !== false) throw new Error("delivery.enabled must be boolean");
|
||||
const noProxy = proxy.noProxy;
|
||||
if (!Array.isArray(noProxy) || noProxy.some((value) => typeof value !== "string" || value.length === 0)) throw new Error("delivery.build.proxy.noProxy must be a non-empty string array");
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
writeValue(outputDir, "enabled", enabled ? "true" : "false");
|
||||
writeValue(outputDir, "dockerfile", required(record(service.repository, `${serviceRef}.repository`).dockerfile, `${serviceRef}.repository.dockerfile`));
|
||||
writeValue(outputDir, "image-repository", required(image.repository, "delivery.image.repository"));
|
||||
writeValue(outputDir, "network-mode", required(build.networkMode, "delivery.build.networkMode"));
|
||||
writeValue(outputDir, "http-proxy", required(proxy.http, "delivery.build.proxy.http"));
|
||||
writeValue(outputDir, "https-proxy", required(proxy.https, "delivery.build.proxy.https"));
|
||||
writeValue(outputDir, "all-proxy", required(proxy.all, "delivery.build.proxy.all"));
|
||||
writeValue(outputDir, "no-proxy", noProxy.join(","));
|
||||
process.stdout.write(`${JSON.stringify({ ok: true, action: "unidesk-host-release-prepare", enabled, serviceRef, valuesPrinted: false })}\n`);
|
||||
}
|
||||
|
||||
if (!process.execArgv.includes("--check")) main();
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bun
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
function option(name) {
|
||||
const index = process.argv.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
const value = process.argv[index + 1];
|
||||
if (value === undefined || value.length === 0 || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function record(value, path) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function required(value, path) {
|
||||
if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${path} must be a non-empty single-line string`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function run(command, args, cwd, allowFailure = false) {
|
||||
const result = spawnSync(command, args, { cwd, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
|
||||
if (result.error !== undefined) throw result.error;
|
||||
if (result.status !== 0 && !allowFailure) {
|
||||
const detail = `${result.stderr || result.stdout || "command failed"}`.trim().slice(-2000);
|
||||
throw new Error(`${command} ${args.join(" ")} failed (${result.status}): ${detail}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function safeManifestPath(value) {
|
||||
const path = required(value, "delivery.gitops.manifestPath");
|
||||
if (path.startsWith("/") || path.split("/").includes("..")) throw new Error("delivery.gitops.manifestPath must be a safe relative path");
|
||||
return path;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const configPath = resolve(option("--config") ?? "config/unidesk-host-k8s.yaml");
|
||||
const sourceRoot = resolve(option("--source-root") ?? process.cwd());
|
||||
const metadataPath = resolve(required(option("--metadata"), "--metadata"));
|
||||
const sourceCommit = required(option("--source-commit"), "--source-commit");
|
||||
const worktree = resolve(option("--worktree") ?? "/workspace/unidesk-host-gitops");
|
||||
if (!/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error("--source-commit must be a full Git commit SHA");
|
||||
|
||||
const config = record(Bun.YAML.parse(readFileSync(configPath, "utf8")), configPath);
|
||||
const delivery = record(config.delivery, "delivery");
|
||||
if (delivery.enabled !== true && delivery.enabled !== false) throw new Error("delivery.enabled must be boolean");
|
||||
const enabled = delivery.enabled === true;
|
||||
const serviceRef = required(delivery.serviceRef, "delivery.serviceRef");
|
||||
const imageConfig = record(delivery.image, "delivery.image");
|
||||
const gitops = record(delivery.gitops, "delivery.gitops");
|
||||
const author = record(gitops.author, "delivery.gitops.author");
|
||||
const readUrl = required(gitops.readUrl, "delivery.gitops.readUrl");
|
||||
const writeUrl = required(gitops.writeUrl, "delivery.gitops.writeUrl");
|
||||
const branch = required(gitops.branch, "delivery.gitops.branch");
|
||||
const manifestPath = safeManifestPath(gitops.manifestPath);
|
||||
let digest = null;
|
||||
let digestRef = null;
|
||||
let manifest = null;
|
||||
|
||||
if (enabled) {
|
||||
const metadata = record(JSON.parse(readFileSync(metadataPath, "utf8")), metadataPath);
|
||||
digest = required(metadata["containerimage.digest"], `${metadataPath}.containerimage.digest`);
|
||||
if (!/^sha256:[0-9a-f]{64}$/u.test(digest)) throw new Error("containerimage.digest must be sha256:<64 hex>");
|
||||
digestRef = `${required(imageConfig.repository, "delivery.image.repository")}@${digest}`;
|
||||
const render = run("bun", [
|
||||
"scripts/native/deploy/render-unidesk-host-service.mjs",
|
||||
"--config",
|
||||
configPath,
|
||||
"--service-ref",
|
||||
serviceRef,
|
||||
"--image",
|
||||
digestRef,
|
||||
"--source-commit",
|
||||
sourceCommit,
|
||||
], sourceRoot);
|
||||
manifest = render.stdout;
|
||||
if (!manifest.includes("kind: Deployment") || !manifest.includes("kind: Service")) throw new Error("service renderer did not emit Service and Deployment objects");
|
||||
}
|
||||
|
||||
rmSync(worktree, { recursive: true, force: true });
|
||||
run("git", ["clone", "--no-checkout", readUrl, worktree], sourceRoot);
|
||||
const fetched = run("git", ["fetch", "origin", branch], worktree, true).status === 0;
|
||||
if (fetched) {
|
||||
run("git", ["checkout", "-B", branch, `origin/${branch}`], worktree);
|
||||
} else {
|
||||
run("git", ["checkout", "--orphan", branch], worktree);
|
||||
run("git", ["rm", "-rf", "."], worktree, true);
|
||||
}
|
||||
|
||||
const targetPath = resolve(worktree, manifestPath);
|
||||
if (!targetPath.startsWith(`${worktree}/`)) throw new Error("resolved manifest path escaped the GitOps worktree");
|
||||
if (enabled && manifest !== null) {
|
||||
mkdirSync(dirname(targetPath), { recursive: true });
|
||||
writeFileSync(targetPath, manifest, "utf8");
|
||||
} else {
|
||||
try {
|
||||
unlinkSync(targetPath);
|
||||
} catch (error) {
|
||||
if (error?.code !== "ENOENT") throw error;
|
||||
}
|
||||
}
|
||||
|
||||
run("git", ["config", "user.name", required(author.name, "delivery.gitops.author.name")], worktree);
|
||||
run("git", ["config", "user.email", required(author.email, "delivery.gitops.author.email")], worktree);
|
||||
run("git", ["add", "-A"], worktree);
|
||||
const changed = run("git", ["diff", "--cached", "--quiet"], worktree, true).status !== 0;
|
||||
if (changed) {
|
||||
const action = enabled ? "deploy" : "remove";
|
||||
run("git", ["commit", "-m", `unidesk-host: ${action} ${serviceRef} ${sourceCommit.slice(0, 12)}`], worktree);
|
||||
}
|
||||
|
||||
run("git", ["remote", "set-url", "origin", writeUrl], worktree);
|
||||
let pushAttempts = 0;
|
||||
if (changed) {
|
||||
let pushed = false;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
pushAttempts = attempt;
|
||||
const push = run("git", ["push", "origin", `HEAD:refs/heads/${branch}`], worktree, true);
|
||||
if (push.status === 0) {
|
||||
pushed = true;
|
||||
break;
|
||||
}
|
||||
run("git", ["fetch", "origin", branch], worktree);
|
||||
const rebase = run("git", ["rebase", `origin/${branch}`], worktree, true);
|
||||
if (rebase.status !== 0) {
|
||||
run("git", ["rebase", "--abort"], worktree, true);
|
||||
throw new Error(`GitOps rebase failed after push attempt ${attempt}`);
|
||||
}
|
||||
}
|
||||
if (!pushed) throw new Error("GitOps push failed after 3 attempts");
|
||||
}
|
||||
|
||||
const head = run("git", ["rev-parse", "HEAD"], worktree, !enabled && !changed);
|
||||
const gitopsCommit = head.status === 0 ? head.stdout.trim() : null;
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
phase: "gitops-publish",
|
||||
status: enabled ? (changed ? "built" : "reused") : "disabled",
|
||||
imageStatus: enabled ? (changed ? "built" : "reused") : "disabled",
|
||||
sourceCommit,
|
||||
digest,
|
||||
digestRef,
|
||||
gitopsCommit,
|
||||
changed,
|
||||
pushAttempts,
|
||||
valuesPrinted: false,
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
if (!process.execArgv.includes("--check")) main();
|
||||
Reference in New Issue
Block a user