feat: add gh pr preflight helper
This commit is contained in:
+277
-3
@@ -3901,6 +3901,264 @@ async function prGraphqlMetadata(repo: string, token: string, number: number): P
|
||||
return pullRequest;
|
||||
}
|
||||
|
||||
function statusContextName(context: GitHubPullRequestGraphqlStatusContext): string {
|
||||
return context.name ?? context.context ?? "(unnamed status check)";
|
||||
}
|
||||
|
||||
function statusContextBucket(context: GitHubPullRequestGraphqlStatusContext): string {
|
||||
const type = context.__typename ?? "StatusContext";
|
||||
const status = (context.status ?? "").toUpperCase();
|
||||
const conclusion = (context.conclusion ?? "").toUpperCase();
|
||||
const state = (context.state ?? "").toUpperCase();
|
||||
if (type === "CheckRun") {
|
||||
if (status.length > 0 && status !== "COMPLETED") return "pending";
|
||||
if (conclusion === "SUCCESS") return "success";
|
||||
if (conclusion === "NEUTRAL") return "neutral";
|
||||
if (conclusion === "SKIPPED") return "skipped";
|
||||
if (conclusion === "CANCELLED") return "cancelled";
|
||||
if (conclusion === "TIMED_OUT") return "timed-out";
|
||||
if (["FAILURE", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE"].includes(conclusion)) return "failure";
|
||||
return "unknown";
|
||||
}
|
||||
if (state === "SUCCESS") return "success";
|
||||
if (state === "PENDING" || state === "EXPECTED") return "pending";
|
||||
if (state === "FAILURE" || state === "ERROR") return "failure";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function compactStatusContext(context: GitHubPullRequestGraphqlStatusContext): Record<string, unknown> {
|
||||
const bucket = statusContextBucket(context);
|
||||
return {
|
||||
name: statusContextName(context),
|
||||
type: context.__typename ?? "StatusContext",
|
||||
bucket,
|
||||
status: context.status ?? null,
|
||||
conclusion: context.conclusion ?? null,
|
||||
state: context.state ?? null,
|
||||
targetUrl: context.targetUrl ?? null,
|
||||
description: context.description === undefined || context.description === null ? null : preview(context.description),
|
||||
};
|
||||
}
|
||||
|
||||
function statusRollupSummary(
|
||||
repo: string,
|
||||
number: number,
|
||||
rollup: GitHubPullRequestGraphqlStatusCheckRollup | null | undefined,
|
||||
includeRaw: boolean,
|
||||
): Record<string, unknown> {
|
||||
const contexts = rollup?.contexts?.nodes ?? [];
|
||||
const compactContexts = contexts.map(compactStatusContext);
|
||||
const counts: Record<string, number> = {
|
||||
success: 0,
|
||||
failure: 0,
|
||||
pending: 0,
|
||||
neutral: 0,
|
||||
skipped: 0,
|
||||
cancelled: 0,
|
||||
"timed-out": 0,
|
||||
unknown: 0,
|
||||
};
|
||||
for (const context of compactContexts) {
|
||||
const bucket = String(context.bucket ?? "unknown");
|
||||
counts[bucket] = (counts[bucket] ?? 0) + 1;
|
||||
}
|
||||
const nonSuccess = compactContexts.filter((context) => context.bucket !== "success");
|
||||
const failing = compactContexts.filter((context) => context.bucket === "failure" || context.bucket === "cancelled" || context.bucket === "timed-out");
|
||||
const pending = compactContexts.filter((context) => context.bucket === "pending" || context.bucket === "unknown");
|
||||
const neutral = compactContexts.filter((context) => context.bucket === "neutral" || context.bucket === "skipped");
|
||||
return {
|
||||
state: rollup?.state ?? null,
|
||||
totalContexts: compactContexts.length,
|
||||
counts,
|
||||
failingContexts: failing.slice(0, 10),
|
||||
pendingContexts: pending.slice(0, 10),
|
||||
neutralContexts: neutral.slice(0, 10),
|
||||
nonSuccessPreview: nonSuccess.slice(0, 12),
|
||||
omittedNonSuccessContexts: Math.max(0, nonSuccess.length - 12),
|
||||
rawOmitted: !includeRaw,
|
||||
...(includeRaw
|
||||
? { contexts: compactContexts, raw: rollup ?? null }
|
||||
: { fullHint: `bun scripts/cli.ts gh pr preflight ${number} --repo ${repo} --full` }),
|
||||
};
|
||||
}
|
||||
|
||||
function compactProbeOk(value: unknown): boolean | null {
|
||||
if (value === "ok") return true;
|
||||
if (value === "skipped") return null;
|
||||
if (isRecord(value) && value.ok === true) return true;
|
||||
if (isRecord(value) && value.ok === false) return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function compactAuthCapability(auth: GitHubCommandResult): Record<string, unknown> {
|
||||
const token = isRecord(auth.token) ? auth.token : {};
|
||||
const gh = isRecord(auth.gh) ? auth.gh : {};
|
||||
const probes = isRecord(auth.probes) ? auth.probes : {};
|
||||
return {
|
||||
ok: auth.ok === true,
|
||||
degradedReason: auth.ok === false ? auth.degradedReason ?? null : null,
|
||||
runnerDisposition: auth.ok === false ? auth.runnerDisposition ?? null : null,
|
||||
tokenPresent: token.present === true,
|
||||
tokenSource: typeof token.source === "string" ? token.source : null,
|
||||
ghFallbackAttempted: token.ghFallbackAttempted === true,
|
||||
ghBinaryFound: gh.binaryFound === true,
|
||||
restApi: compactProbeOk(probes.restApi),
|
||||
repoVisible: compactProbeOk(probes.repo),
|
||||
issueRead: compactProbeOk(probes.issueRead),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function prPreflightPolicy(repo: string, number: number): Record<string, unknown> {
|
||||
return {
|
||||
readOnly: true,
|
||||
writesRemote: false,
|
||||
createsPr: false,
|
||||
comments: false,
|
||||
mergesPr: false,
|
||||
mergeCommandSupported: false,
|
||||
unsupportedMergeCommand: `bun scripts/cli.ts gh pr merge ${number} --repo ${repo}`,
|
||||
unideskCliMergeSupported: false,
|
||||
ordinaryRunnerFinalActionAllowed: true,
|
||||
note: "This preflight only reads GitHub auth, PR metadata, mergeability, and status checks; the UniDesk REST CLI still never merges PRs.",
|
||||
};
|
||||
}
|
||||
|
||||
function preflightPullRequestSummary(summary: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
number: summary.number ?? null,
|
||||
title: summary.title ?? null,
|
||||
state: summary.state ?? null,
|
||||
stateDetail: summary.stateDetail ?? null,
|
||||
draft: summary.draft ?? null,
|
||||
url: summary.url ?? null,
|
||||
author: summary.author ?? null,
|
||||
head: summary.head ?? null,
|
||||
base: summary.base ?? null,
|
||||
headRefName: summary.headRefName ?? null,
|
||||
baseRefName: summary.baseRefName ?? null,
|
||||
closed: summary.closed ?? null,
|
||||
merged: summary.merged ?? null,
|
||||
mergedAt: summary.mergedAt ?? null,
|
||||
updatedAt: summary.updatedAt ?? null,
|
||||
bodyOmitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
function prCloseoutSummary(
|
||||
pr: Record<string, unknown>,
|
||||
metadata: Record<string, unknown>,
|
||||
statusChecks: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const blockers: string[] = [];
|
||||
const pending: string[] = [];
|
||||
const prState = typeof pr.state === "string" ? pr.state.toLowerCase() : "";
|
||||
if (prState !== "open") blockers.push(`state:${pr.state ?? "unknown"}`);
|
||||
if (pr.draft === true) blockers.push("draft:true");
|
||||
const mergeable = typeof metadata.mergeable === "string" ? metadata.mergeable.toUpperCase() : null;
|
||||
if (mergeable === "MERGEABLE") {
|
||||
// Ready signal, combined with mergeStateStatus and checks below.
|
||||
} else if (mergeable === "CONFLICTING" || mergeable === "NOT_MERGEABLE") {
|
||||
blockers.push(`mergeable:${mergeable}`);
|
||||
} else {
|
||||
pending.push(`mergeable:${mergeable ?? "unknown"}`);
|
||||
}
|
||||
const mergeStateStatus = typeof metadata.mergeStateStatus === "string" ? metadata.mergeStateStatus.toUpperCase() : null;
|
||||
if (mergeStateStatus === "CLEAN") {
|
||||
// Ready signal.
|
||||
} else if (mergeStateStatus === "UNKNOWN" || mergeStateStatus === null) {
|
||||
pending.push(`mergeStateStatus:${mergeStateStatus ?? "unknown"}`);
|
||||
} else {
|
||||
blockers.push(`mergeStateStatus:${mergeStateStatus}`);
|
||||
}
|
||||
const rollupState = typeof statusChecks.state === "string" ? statusChecks.state.toUpperCase() : null;
|
||||
const totalContexts = typeof statusChecks.totalContexts === "number" ? statusChecks.totalContexts : 0;
|
||||
const counts = isRecord(statusChecks.counts) ? statusChecks.counts : {};
|
||||
const failureCount = Number(counts.failure ?? 0) + Number(counts.cancelled ?? 0) + Number(counts["timed-out"] ?? 0);
|
||||
const pendingCount = Number(counts.pending ?? 0) + Number(counts.unknown ?? 0);
|
||||
if (failureCount > 0) blockers.push(`statusChecks:failure:${failureCount}`);
|
||||
if (pendingCount > 0) pending.push(`statusChecks:pending:${pendingCount}`);
|
||||
if (rollupState === "SUCCESS" || rollupState === null && totalContexts === 0) {
|
||||
// Ready signal.
|
||||
} else if (rollupState === "PENDING" || rollupState === "EXPECTED" || rollupState === null) {
|
||||
pending.push(`statusCheckRollup:${rollupState ?? "unknown"}`);
|
||||
} else if (rollupState !== null) {
|
||||
blockers.push(`statusCheckRollup:${rollupState}`);
|
||||
}
|
||||
const readyForCommanderMerge = blockers.length === 0 && pending.length === 0;
|
||||
return {
|
||||
readyForCommanderMerge,
|
||||
conclusion: readyForCommanderMerge ? "ready" : blockers.length > 0 ? "blocked" : "pending",
|
||||
blockers,
|
||||
pending,
|
||||
commanderAction: readyForCommanderMerge
|
||||
? "review and merge through a repo-owned GitHub path when task boundaries allow; UniDesk REST gh pr merge remains unsupported"
|
||||
: "resolve blockers or rerun after GitHub finishes computing mergeability/status checks",
|
||||
};
|
||||
}
|
||||
|
||||
async function prPreflight(repo: string, number: number, commandName: "preflight" | "pr preflight" | "pr closeout", includeRaw: boolean): Promise<GitHubCommandResult> {
|
||||
const auth = await authStatus(repo);
|
||||
const authCapability = compactAuthCapability(auth);
|
||||
const policy = prPreflightPolicy(repo, number);
|
||||
if (auth.ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
command: commandName,
|
||||
repo,
|
||||
number,
|
||||
degradedReason: auth.degradedReason,
|
||||
runnerDisposition: auth.runnerDisposition,
|
||||
authCapability,
|
||||
policy,
|
||||
details: auth,
|
||||
};
|
||||
}
|
||||
const { token, probe } = resolveToken(true);
|
||||
const missing = authRequired(repo, commandName, probe);
|
||||
if (missing !== null || token === null) {
|
||||
const missingResult = missing ?? authRequired(repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
|
||||
return {
|
||||
...(missingResult ?? commandError(commandName, repo, errorPayload("missing-token", "GH_TOKEN or GITHUB_TOKEN is required"))),
|
||||
command: commandName,
|
||||
number,
|
||||
authCapability,
|
||||
policy,
|
||||
} as GitHubCommandResult;
|
||||
}
|
||||
const { owner, name } = repoParts(repo);
|
||||
const pr = await githubRequest<GitHubPullRequest>(token, "GET", `/repos/${owner}/${name}/pulls/${number}`);
|
||||
if (isGitHubError(pr)) return commandError(commandName, repo, pr, { number, authCapability, policy });
|
||||
const metadata = await prGraphqlMetadata(repo, token, number);
|
||||
const summary = prSummary(pr);
|
||||
const pullRequest = preflightPullRequestSummary(summary);
|
||||
if (isGitHubError(metadata)) return commandError(commandName, repo, metadata, { number, phase: "fetch-pr-closeout-metadata", pullRequest, authCapability, policy });
|
||||
const metadataSummary = prMetadataSummary(metadata);
|
||||
const statusChecks = statusRollupSummary(repo, number, metadata.statusCheckRollup, includeRaw);
|
||||
const closeout = prCloseoutSummary(summary, metadataSummary, statusChecks);
|
||||
return {
|
||||
ok: true,
|
||||
command: commandName,
|
||||
canonicalCommand: `bun scripts/cli.ts gh pr preflight ${number} --repo ${repo}`,
|
||||
aliases: ["bun scripts/cli.ts gh preflight <number>", "bun scripts/cli.ts gh pr closeout <number>"],
|
||||
repo,
|
||||
number,
|
||||
readOnly: true,
|
||||
writesRemote: false,
|
||||
valuesPrinted: false,
|
||||
authCapability,
|
||||
pullRequest,
|
||||
mergeability: {
|
||||
mergeable: metadataSummary.mergeable,
|
||||
mergeStateStatus: metadataSummary.mergeStateStatus,
|
||||
...closeout,
|
||||
},
|
||||
statusChecks,
|
||||
policy,
|
||||
...(includeRaw ? { raw: { authStatus: auth, pullRequest, closeoutMetadata: metadataSummary } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function repoSummary(repo: GitHubRepository): Record<string, unknown> {
|
||||
return {
|
||||
id: repo.id ?? null,
|
||||
@@ -5238,11 +5496,14 @@ export function ghHelp(): unknown {
|
||||
"bun scripts/cli.ts gh issue board-row upsert <issueNumber> [--repo owner/name] --board-issue 20 --section open|closed [--category text] --branch <branch> --tasks <task> --summary <text> --focus <text> --validation <text> --progress <text> [--status OPEN|CLOSED] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row move <issueNumber> [--repo owner/name] --board-issue 20 --to open|closed [--status OPEN|CLOSED] [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh issue board-row delete <issueNumber> [--repo owner/name] --board-issue 20 [--dry-run] [--expect-body-sha sha256]",
|
||||
"bun scripts/cli.ts gh preflight <prNumber> [--repo owner/name] [--full|--raw] [compatibility alias for gh pr preflight]",
|
||||
"bun scripts/cli.ts gh pr list [--repo owner/name] [--state open|closed|all] [--limit N] [--json number,title,state,url,updatedAt,createdAt,author,head,base,draft]",
|
||||
"bun scripts/cli.ts gh pr files <number> [--repo owner/name] [--limit N]",
|
||||
"bun scripts/cli.ts gh pr diff <number> --stat [--repo owner/name] [--limit N] [compatibility alias for pr files; no raw diff]",
|
||||
"bun scripts/cli.ts gh pr read <number|owner/repo#number> [--repo owner/name] [--json body,title,state,stateDetail,closed,closedAt,merged,mergedAt,mergeCommit,head,base,draft,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup] [--raw|--full]",
|
||||
"bun scripts/cli.ts gh pr view <number|owner/repo#number> [--repo owner/name] [--raw|--full] [compatibility alias for pr read]",
|
||||
"bun scripts/cli.ts gh pr preflight <number> [--repo owner/name] [--full|--raw]",
|
||||
"bun scripts/cli.ts gh pr closeout <number> [--repo owner/name] [--full|--raw] [compatibility alias for pr preflight]",
|
||||
"bun scripts/cli.ts gh pr create --title <title> --body-file <file>|--body <text> --base <branch> --head <branch> [--repo owner/name] [--draft] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr edit <number> [--title title] [--body-file <file>|--body-file -|--body <text>] [--repo owner/name] [--dry-run]",
|
||||
"bun scripts/cli.ts gh pr update <number> --mode replace|append [--body-file <file>|--body-file -|--body <text>] [--title title] [--repo owner/name] [--dry-run]",
|
||||
@@ -5279,6 +5540,7 @@ export function ghHelp(): unknown {
|
||||
"PR files is the canonical compact changed-file/stat summary. It uses GitHub REST, returns bounded file rows, additions/deletions/changes when available, truncation metadata, and a next command for full details. Raw diff patches are not emitted by default; gh pr diff <number> --stat is a compatibility alias for the same JSON summary.",
|
||||
"PR edit/update PATCHes /repos/{owner}/{repo}/pulls/{number} through REST only, never GitHub Projects Classic GraphQL/projectCards, and returns low-noise JSON with repo, PR number, changedFields, url, and body size/SHA metadata instead of echoing the full body.",
|
||||
"PR read is the canonical read path; view remains a compatibility alias. PR read/view accept owner/repo#number shorthand and derive --repo unless an explicit conflicting --repo is supplied, which fails structurally with suggested commands. PR read/view supports REST closeout fields stateDetail, closed, closedAt, merged, mergedAt, mergeCommit, headRefName, and baseRefName; mergeable, mergeStateStatus, and statusCheckRollup are fetched through GitHub GraphQL only when requested or when --raw/--full requests full disclosure.",
|
||||
"PR preflight is a low-noise read-only closeout helper. It combines redacted auth capability, PR branch/state metadata, mergeability, mergeStateStatus, compact status check counts, and the explicit UniDesk REST CLI no-merge policy. Use --full or --raw to include all fetched status contexts.",
|
||||
"PR create/edit/update/comment are safe-write operations with dry-run planning; merge is intentionally unsupported in this phase.",
|
||||
],
|
||||
};
|
||||
@@ -5313,14 +5575,15 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
return validationError(command, options.repo, "--json field selection is only supported by gh issue read/view/list and gh pr read/view/list");
|
||||
}
|
||||
}
|
||||
if ((optionWasProvided(args, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && isIssueReadCommand(sub)) || (top === "pr" && isPrReadCommand(sub)))) {
|
||||
if ((optionWasProvided(args, "--raw") || optionWasProvided(args, "--full")) && !((top === "issue" && isIssueReadCommand(sub)) || top === "preflight" || (top === "pr" && (isPrReadCommand(sub) || sub === "preflight" || sub === "closeout")))) {
|
||||
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
|
||||
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue read/view and gh pr read/view.", {
|
||||
return validationError(command, options.repo, "--raw and --full are explicit full-disclosure aliases only for gh issue read/view, gh pr read/view, and gh pr preflight/closeout.", {
|
||||
supportedCommands: [
|
||||
"bun scripts/cli.ts gh issue read owner/name#<number> --raw",
|
||||
"bun scripts/cli.ts gh issue read <number> --repo owner/name --json body,title,state,comments",
|
||||
"bun scripts/cli.ts gh pr read owner/name#<number> --raw",
|
||||
`bun scripts/cli.ts gh pr read <number> --repo owner/name --json ${readViewSupportedJsonFields("pr")}`,
|
||||
"bun scripts/cli.ts gh pr preflight <number> --repo owner/name --full",
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -5380,6 +5643,12 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
|
||||
if (top === "auth" && sub === "status") return authStatus(options.repo);
|
||||
|
||||
if (top === "preflight") {
|
||||
const number = parseNumberForCommand(options.repo, sub, "preflight");
|
||||
if (typeof number !== "number") return number;
|
||||
return prPreflight(options.repo, number, "preflight", options.full || options.raw);
|
||||
}
|
||||
|
||||
if (top === "issue") {
|
||||
if (sub === "delete") return unsupportedCommand("issue delete", options.repo, "GitHub REST does not support hard-deleting issues; use gh issue close for lifecycle deletion semantics.");
|
||||
if (sub === "comment" && third === "delete") {
|
||||
@@ -5498,6 +5767,11 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
return prFiles(options.repo, token, number, options.limit, "pr files");
|
||||
}
|
||||
if (sub === "delete") return unsupportedCommand("pr delete", options.repo, "GitHub REST does not support hard-deleting pull requests; use gh pr close for lifecycle deletion semantics.");
|
||||
if (sub === "preflight" || sub === "closeout") {
|
||||
const number = parseNumberForCommand(options.repo, third, `pr ${sub}`);
|
||||
if (typeof number !== "number") return number;
|
||||
return prPreflight(options.repo, number, sub === "closeout" ? "pr closeout" : "pr preflight", options.full || options.raw);
|
||||
}
|
||||
if (sub === "comment" && third === "delete") {
|
||||
const commentId = parseNumberForCommand(options.repo, args[3], "pr comment delete");
|
||||
if (typeof commentId !== "number") return commentId;
|
||||
@@ -5573,7 +5847,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
|
||||
);
|
||||
}
|
||||
if (sub !== "list" && !isPrReadCommand(sub)) {
|
||||
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, files, diff --stat, read/view, create, update/edit, close, reopen, comment create/delete, and unsupported merge/delete.");
|
||||
return unsupportedCommand(`pr ${sub ?? ""}`.trim(), options.repo, "PR supported commands are list, files, diff --stat, read/view, preflight/closeout, create, update/edit, close, reopen, comment create/delete, and unsupported merge/delete.");
|
||||
}
|
||||
if (sub === "read" || sub === "view") {
|
||||
const resolved = resolveReadViewNumberReference("pr", sub, third, options, args);
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
|
||||
{ command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
|
||||
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, hard delete unsupported, and merge blocked." },
|
||||
{ command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and merge blocked." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
|
||||
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
|
||||
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
|
||||
|
||||
Reference in New Issue
Block a user