deploy: configure NC01 HWLAB monitor exposure

This commit is contained in:
root
2026-07-08 05:34:11 +02:00
parent 5dfb78f000
commit a328aa909e
41 changed files with 2522 additions and 110 deletions
+149 -4
View File
@@ -3,10 +3,25 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { isAbsolute, join, normalize, relative, resolve } from "node:path";
import { rootPath } from "../config";
import { parseEnvFile } from "../platform-infra-ops-library";
import { readIssueCommentBody } from "./body-input";
import { PREVIEW_CHARS } from "./types";
import { PREVIEW_CHARS, UNIDESK_CLI_CONFIG_PATH } from "./types";
import type { GitHubOptions, GitHubTokenProbe } from "./types";
const GITHUB_TOKEN_SOURCE_CONFIG_REF = `${UNIDESK_CLI_CONFIG_PATH}#github.auth.tokenSource`;
interface GitHubYamlTokenSourceConfig {
enabled: boolean;
priority: "before-env" | "after-env";
root: string;
sourceRef: string;
sourceKey: string;
format: "env" | "raw-token";
}
export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
if (options.comment === undefined && options.commentFile === undefined) return null;
if (options.comment !== undefined) {
@@ -34,6 +49,50 @@ export function tokenFromEnvironment(): GitHubTokenProbe {
return { present: false, source: null, ghFallbackAttempted: false };
}
export function tokenFromYamlSource(): { token: string | null; probe: GitHubTokenProbe } {
const config = readGitHubYamlTokenSourceConfig();
if (config === null || !config.enabled) {
return {
token: null,
probe: {
present: false,
source: null,
ghFallbackAttempted: false,
yamlSourceAttempted: true,
yamlSourceEnabled: config?.enabled ?? false,
yamlSourcePriority: config?.priority,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
valuesPrinted: false,
},
};
}
const sourceRoot = resolveYamlSourceRoot(config.root);
const sourcePath = resolveYamlSourcePath(sourceRoot, config.sourceRef);
const sourceExists = existsSync(sourcePath);
const values = sourceExists ? parseYamlTokenSource(readFileSync(sourcePath, "utf8"), config) : {};
const token = values[config.sourceKey] ?? null;
const present = token !== null && token.length > 0;
return {
token: present ? token : null,
probe: {
present,
source: present ? "yaml-token-source" : null,
ghFallbackAttempted: false,
yamlSourceAttempted: true,
yamlSourceEnabled: true,
yamlSourcePriority: config.priority,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
sourceRef: config.sourceRef,
sourceKey: config.sourceKey,
sourcePath: redactRepoPath(sourcePath),
sourceExists,
sourceKeyPresent: present,
tokenFingerprint: present ? fingerprintToken(token) : null,
valuesPrinted: false,
},
};
}
export function ghBinaryPath(): string | null {
try {
const output = execFileSync("sh", ["-lc", "command -v gh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
@@ -53,17 +112,103 @@ export function ghAuthToken(): string | null {
}
export function resolveToken(allowGhFallback: boolean): { token: string | null; probe: GitHubTokenProbe } {
const yamlToken = tokenFromYamlSource();
if (yamlToken.probe.yamlSourcePriority === "before-env" && yamlToken.probe.present) return yamlToken;
const envProbe = tokenFromEnvironment();
if (envProbe.present) {
const token = envProbe.source === "GH_TOKEN" ? process.env.GH_TOKEN ?? null : process.env.GITHUB_TOKEN ?? null;
return { token, probe: envProbe };
}
if (!allowGhFallback) return { token: null, probe: envProbe };
if (yamlToken.probe.present) return yamlToken;
if (!allowGhFallback) return { token: null, probe: yamlToken.probe };
const ghPath = ghBinaryPath();
if (ghPath === null) return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
if (ghPath === null) return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
const token = ghAuthToken();
if (token !== null) return { token, probe: { present: true, source: "gh-auth-token", ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: true } };
return { token: null, probe: { present: false, source: null, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
}
function readGitHubYamlTokenSourceConfig(): GitHubYamlTokenSourceConfig | null {
const configPath = rootPath(UNIDESK_CLI_CONFIG_PATH);
if (!existsSync(configPath)) return null;
const parsed = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, UNIDESK_CLI_CONFIG_PATH);
const github = optionalRecord(parsed.github, `${UNIDESK_CLI_CONFIG_PATH}.github`);
const auth = optionalRecord(github?.auth, `${UNIDESK_CLI_CONFIG_PATH}.github.auth`);
const tokenSource = optionalRecord(auth?.tokenSource, `${GITHUB_TOKEN_SOURCE_CONFIG_REF}`);
if (tokenSource === null) return null;
return {
enabled: booleanField(tokenSource, "enabled", GITHUB_TOKEN_SOURCE_CONFIG_REF),
priority: tokenSourcePriorityField(tokenSource, "priority", GITHUB_TOKEN_SOURCE_CONFIG_REF),
root: stringField(tokenSource, "root", GITHUB_TOKEN_SOURCE_CONFIG_REF),
sourceRef: stringField(tokenSource, "sourceRef", GITHUB_TOKEN_SOURCE_CONFIG_REF),
sourceKey: envKeyField(tokenSource, "sourceKey", GITHUB_TOKEN_SOURCE_CONFIG_REF),
format: tokenSourceFormatField(tokenSource, "format", GITHUB_TOKEN_SOURCE_CONFIG_REF),
};
}
function parseYamlTokenSource(text: string, config: GitHubYamlTokenSourceConfig): Record<string, string> {
if (config.format === "raw-token") return { [config.sourceKey]: text.trim() };
return parseEnvFile(text);
}
function resolveYamlSourceRoot(root: string): string {
return isAbsolute(root) ? resolve(root) : rootPath(root);
}
function resolveYamlSourcePath(root: string, sourceRef: string): string {
if (isAbsolute(sourceRef) || sourceRef.includes("..")) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef must be a relative path without ..`);
const resolved = normalize(join(root, sourceRef));
if (resolved !== root && !resolved.startsWith(`${root}/`)) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef escapes configured root`);
return resolved;
}
function redactRepoPath(value: string): string {
const root = rootPath();
return value === root ? "." : value.startsWith(`${root}/`) ? relative(root, value) : value;
}
function fingerprintToken(value: string): string {
return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
}
function optionalRecord(value: unknown, path: string): Record<string, unknown> | null {
if (value === undefined || value === null) return null;
return asRecord(value, path);
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value.trim();
}
function envKeyField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${path}.${key} must be an env key`);
return value;
}
function tokenSourcePriorityField(obj: Record<string, unknown>, key: string, path: string): "before-env" | "after-env" {
const value = stringField(obj, key, path);
if (value !== "before-env" && value !== "after-env") throw new Error(`${path}.${key} must be before-env or after-env`);
return value;
}
function tokenSourceFormatField(obj: Record<string, unknown>, key: string, path: string): "env" | "raw-token" {
const value = obj[key] === undefined ? "env" : stringField(obj, key, path);
if (value !== "env" && value !== "raw-token") throw new Error(`${path}.${key} must be env or raw-token`);
return value;
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
return value;
}
export function repoParts(repo: string): { owner: string; name: string } {
+1 -1
View File
@@ -29,7 +29,7 @@ export async function authStatus(repo: string): Promise<GitHubCommandResult> {
};
}
degraded.push("missing-token");
return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required", { details: probe }), {
return commandError("auth status", repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: probe }), {
degraded,
gh: { binaryFound: ghPath !== null, path: ghPath },
token: probe,
+3 -3
View File
@@ -335,12 +335,12 @@ export async function githubGraphqlRequest<T>(
export function authRequired(repo: string, command: string, tokenProbe: GitHubTokenProbe): GitHubCommandResult | null {
if (!tokenProbe.present) {
if (tokenProbe.ghFallbackAttempted && tokenProbe.ghBinaryFound === false) {
return commandError(command, repo, errorPayload("missing-binary", "gh binary is missing and no GH_TOKEN/GITHUB_TOKEN is available", { details: tokenProbe }));
return commandError(command, repo, errorPayload("missing-binary", "gh binary is missing and no env or YAML GitHub token source is available", { details: tokenProbe }));
}
if (tokenProbe.ghFallbackAttempted && tokenProbe.ghBinaryFound === true && tokenProbe.ghAuthTokenAvailable === false) {
return commandError(command, repo, errorPayload("auth-failed", "gh auth token failed and no GH_TOKEN/GITHUB_TOKEN is available", { details: tokenProbe }));
return commandError(command, repo, errorPayload("auth-failed", "gh auth token failed and no env or YAML GitHub token source is available", { details: tokenProbe }));
}
return commandError(command, repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required", { details: tokenProbe }), {
return commandError(command, repo, errorPayload("missing-token", "GH_TOKEN, GITHUB_TOKEN, or the YAML-declared GitHub token source is required", { details: tokenProbe }), {
degraded: ["missing-token"],
token: tokenProbe,
});
+10 -1
View File
@@ -377,8 +377,17 @@ function renderAuthStatus(result: GitHubCommandResult): string {
const token = record(result.token);
const gh = record(result.gh);
const probes = record(result.probes);
const tokenDetail = [
`source=${ghText(token.source)}`,
`yaml=${ghText(token.yamlSourceEnabled)}`,
`priority=${ghText(token.yamlSourcePriority)}`,
`sourceRef=${ghText(token.sourceRef)}`,
`key=${ghText(token.sourceKey)}`,
`fingerprint=${ghText(token.tokenFingerprint)}`,
`ghFallback=${ghText(token.ghFallbackAttempted)}`,
].filter((item) => !item.endsWith("=-") && !item.endsWith("=undefined")).join(" ");
const rows = [
["token", token.present === true ? "ok" : "missing", `source=${ghText(token.source)} ghFallback=${ghText(token.ghFallbackAttempted)}`],
["token", token.present === true ? "ok" : "missing", tokenDetail],
["gh-binary", gh.binaryFound === true ? "ok" : "missing", `path=${ghText(gh.path)}`],
["rest-api", probeStatus(probes.restApi), ghText(probes.restApi)],
["repo", probeStatus(probes.repo), probeDetail(probes.repo)],
+12 -1
View File
@@ -410,10 +410,21 @@ export type GitHubCommandFailure = GitHubCommandResult & { ok: false };
export interface GitHubTokenProbe {
present: boolean;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "gh-auth-token" | null;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "yaml-token-source" | "gh-auth-token" | null;
ghFallbackAttempted: boolean;
ghBinaryFound?: boolean;
ghAuthTokenAvailable?: boolean | null;
yamlSourceAttempted?: boolean;
yamlSourceEnabled?: boolean;
yamlSourcePriority?: "before-env" | "after-env";
configRef?: string;
sourceRef?: string;
sourceKey?: string;
sourcePath?: string;
sourceExists?: boolean;
sourceKeyPresent?: boolean;
tokenFingerprint?: string | null;
valuesPrinted?: false;
}
export interface GitHubOptions {