Merge remote-tracking branch 'origin/master' into fix/1923-cicd-buildkit-evidence

This commit is contained in:
Codex
2026-07-13 16:31:14 +02:00
9 changed files with 257 additions and 72 deletions
+7
View File
@@ -14,6 +14,13 @@ github:
sourceRef: gh_token.txt
sourceKey: GH_TOKEN
format: raw-token
repositoryOverrides:
- repository: pikainc/selfmedia
priority: before-env
root: /root/.unidesk/.env
sourceRef: pikainc-selfmedia-gh-token.txt
sourceKey: GH_TOKEN
format: raw-token
prMerge:
unknownRetry:
maxAttempts: 5
@@ -89,3 +89,17 @@
- JD01 未引入 selfmedia key,保持仅使用全局 `github-token`
- 旧 revision `49e7425b` 的失败 PreSync Job 早于 bootstrap,后续只允许正常 source PR 生成新 revision,禁止手工控制面操作。
- R1.1 继续保持 `in_progress`
## 本轮 unidesk-gh 仓库级 authority
- `config/unidesk-cli.yaml#github.auth.repositoryOverrides` 新增通用 exact repository 列表结构,声明 `repository``root``sourceRef``sourceKey``format``priority`;代码未硬编码 `pikainc/selfmedia`
- `pikainc/selfmedia` 精确命中 `pikainc-selfmedia-gh-token.txt`,未命中的 `pikasTech/unidesk``pikasTech/HWLAB` 与其他仓库继续使用全局 `gh_token.txt`
- gh token resolver 以最终 repo 为输入;issue、PR、repo、auth、preflight 与 merge 路径统一选择,positional `owner/repo#number` 在解析完成后才读取凭据。
- `before-env` repository override 优先于全局 `GH_TOKEN``GITHUB_TOKEN`;未命中 override 时保留原全局 YAML、环境变量与 gh fallback 顺序。
- 输出仅披露 `scope``configRef``sourceRef``sourceKey`、presence、fingerprint 与 `valuesPrinted=false`;不再披露完整 source path,错误保持有界。
- `bun test scripts/src/gh-token-repository-override.test.ts`3/3 通过,覆盖 selfmedia 独立 token、unidesk/其他仓库全局 token、before-env 与 positional final-repo 隔离。
- `bun test scripts/src/gh-token-repository-override.test.ts scripts/src/gh-default-render.test.ts`10/10 通过;生产文件 `bun --check`、旧签名扫描与 `git diff --check` 通过。
- Target 原入口 `bun scripts/cli.ts gh auth status --repo pikainc/selfmedia``scope=repository-override``sourceRef=pikainc-selfmedia-gh-token.txt``presence=true`、独立 fingerprint、`valuesPrinted=false`REST/repo/issue-read 均成功。
- Target 原入口 `bun scripts/cli.ts gh auth status --repo pikasTech/unidesk``scope=global``sourceRef=gh_token.txt``presence=true`、全局 fingerprint、`valuesPrinted=false`REST/repo/issue-read 均成功。
- 两仓 fingerprint 不同,证明选择未串线;本轮未执行任何 GitHub issue/PR 写入、运行面操作、部署或合并。
- 本轮源码分支为 `feat/selfmedia-gh-token-authority`R1.1 继续保持 `in_progress`
@@ -0,0 +1,102 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { resolveToken, type GitHubYamlAuthConfig } from "./gh/auth-and-safety";
import { resolvePositionalIssueReference, resolvePositionalPrReference } from "./gh/refs";
import type { GitHubOptions, GitHubResolvedNumberReference } from "./gh/types";
const originalGhToken = process.env.GH_TOKEN;
const originalGithubToken = process.env.GITHUB_TOKEN;
const temporaryRoots: string[] = [];
afterEach(() => {
if (originalGhToken === undefined) delete process.env.GH_TOKEN;
else process.env.GH_TOKEN = originalGhToken;
if (originalGithubToken === undefined) delete process.env.GITHUB_TOKEN;
else process.env.GITHUB_TOKEN = originalGithubToken;
while (temporaryRoots.length > 0) rmSync(temporaryRoots.pop()!, { recursive: true, force: true });
});
function testAuthConfig(): GitHubYamlAuthConfig {
const root = mkdtempSync("/tmp/unidesk-gh-token-override-");
temporaryRoots.push(root);
writeFileSync(join(root, "global-token.txt"), "global-token\n");
writeFileSync(join(root, "selfmedia-token.txt"), "selfmedia-token\n");
return {
tokenSource: {
enabled: true,
priority: "before-env",
root,
sourceRef: "global-token.txt",
sourceKey: "GH_TOKEN",
format: "raw-token",
scope: "global",
configRef: "test#github.auth.tokenSource",
},
repositoryOverrides: [{
repository: "pikainc/selfmedia",
priority: "before-env",
root,
sourceRef: "selfmedia-token.txt",
sourceKey: "GH_TOKEN",
format: "raw-token",
scope: "repository-override",
configRef: "test#github.auth.repositoryOverrides[0]",
}],
};
}
function options(repo: string): GitHubOptions {
return { repo } as GitHubOptions;
}
function resolvedRepo(value: GitHubResolvedNumberReference | { ok: boolean }): string {
if (!("number" in value)) throw new Error("expected a resolved positional reference");
return value.repo;
}
describe("GitHub repository token overrides", () => {
test("selects the exact repository override without affecting global repositories", () => {
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
const authConfig = testAuthConfig();
const selfmedia = resolveToken("pikainc/selfmedia", false, authConfig);
const unidesk = resolveToken("pikasTech/unidesk", false, authConfig);
const other = resolveToken("owner/other", false, authConfig);
expect(selfmedia.token).toBe("selfmedia-token");
expect(selfmedia.probe).toMatchObject({ scope: "repository-override", sourceRef: "selfmedia-token.txt", sourceKey: "GH_TOKEN", valuesPrinted: false });
expect(unidesk.token).toBe("global-token");
expect(other.token).toBe("global-token");
expect(unidesk.probe).toMatchObject({ scope: "global", sourceRef: "global-token.txt", valuesPrinted: false });
expect(other.probe).toMatchObject({ scope: "global", sourceRef: "global-token.txt", valuesPrinted: false });
expect(selfmedia.probe.tokenFingerprint).not.toBe(unidesk.probe.tokenFingerprint);
});
test("keeps before-env repository overrides ahead of global environment tokens", () => {
process.env.GH_TOKEN = "environment-token";
process.env.GITHUB_TOKEN = "secondary-environment-token";
const selected = resolveToken("pikainc/selfmedia", false, testAuthConfig());
expect(selected.token).toBe("selfmedia-token");
expect(selected.probe).toMatchObject({ source: "yaml-token-source", scope: "repository-override", yamlSourcePriority: "before-env" });
});
test("uses the final positional repository for issue and PR token selection", () => {
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
const authConfig = testAuthConfig();
const issueRef = resolvePositionalIssueReference(["issue", "edit", "pikainc/selfmedia#12"], 2, "issue edit", options("pikasTech/unidesk"));
const prRef = resolvePositionalPrReference(["pr", "comment", "pikainc/selfmedia#34"], 2, "pr comment", options("pikasTech/unidesk"));
const issueToken = resolveToken(resolvedRepo(issueRef), false, authConfig);
const prToken = resolveToken(resolvedRepo(prRef), false, authConfig);
const defaultToken = resolveToken("pikasTech/unidesk", false, authConfig);
expect(issueToken.token).toBe("selfmedia-token");
expect(prToken.token).toBe("selfmedia-token");
expect(defaultToken.token).toBe("global-token");
});
});
+59 -20
View File
@@ -4,7 +4,7 @@
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 { isAbsolute, join, normalize, resolve } from "node:path";
import { rootPath } from "../config";
import { parseEnvFile } from "../platform-infra-ops-library";
import { readIssueCommentBody } from "./body-input";
@@ -12,14 +12,23 @@ 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`;
const GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF = `${UNIDESK_CLI_CONFIG_PATH}#github.auth.repositoryOverrides`;
interface GitHubYamlTokenSourceConfig {
enabled: boolean;
export interface GitHubYamlTokenSourceConfig {
enabled?: boolean;
priority: "before-env" | "after-env";
root: string;
sourceRef: string;
sourceKey: string;
format: "env" | "raw-token";
scope: "repository-override" | "global";
configRef: string;
repository?: string;
}
export interface GitHubYamlAuthConfig {
tokenSource: GitHubYamlTokenSourceConfig | null;
repositoryOverrides: GitHubYamlTokenSourceConfig[];
}
export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
@@ -49,19 +58,21 @@ 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) {
export function tokenFromYamlSource(repo: string, authConfig: GitHubYamlAuthConfig = readGitHubYamlAuthConfig()): { token: string | null; probe: GitHubTokenProbe } {
const override = authConfig.repositoryOverrides.find((candidate) => candidate.repository === repo) ?? null;
const config = override ?? authConfig.tokenSource;
if (config === null || config.enabled === false) {
return {
token: null,
probe: {
present: false,
source: null,
scope: override === null ? "global" : "repository-override",
ghFallbackAttempted: false,
yamlSourceAttempted: true,
yamlSourceEnabled: config?.enabled ?? false,
yamlSourcePriority: config?.priority,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
configRef: config?.configRef ?? GITHUB_TOKEN_SOURCE_CONFIG_REF,
valuesPrinted: false,
},
};
@@ -77,14 +88,14 @@ export function tokenFromYamlSource(): { token: string | null; probe: GitHubToke
probe: {
present,
source: present ? "yaml-token-source" : null,
scope: config.scope,
ghFallbackAttempted: false,
yamlSourceAttempted: true,
yamlSourceEnabled: true,
yamlSourcePriority: config.priority,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
configRef: config.configRef,
sourceRef: config.sourceRef,
sourceKey: config.sourceKey,
sourcePath: redactRepoPath(sourcePath),
sourceExists,
sourceKeyPresent: present,
tokenFingerprint: present ? fingerprintToken(token) : null,
@@ -111,8 +122,8 @@ export function ghAuthToken(): string | null {
}
}
export function resolveToken(allowGhFallback: boolean): { token: string | null; probe: GitHubTokenProbe } {
const yamlToken = tokenFromYamlSource();
export function resolveToken(repo: string, allowGhFallback: boolean, authConfig?: GitHubYamlAuthConfig): { token: string | null; probe: GitHubTokenProbe } {
const yamlToken = tokenFromYamlSource(repo, authConfig);
if (yamlToken.probe.yamlSourcePriority === "before-env" && yamlToken.probe.present) return yamlToken;
const envProbe = tokenFromEnvironment();
if (envProbe.present) {
@@ -128,22 +139,43 @@ export function resolveToken(allowGhFallback: boolean): { token: string | null;
return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
}
function readGitHubYamlTokenSourceConfig(): GitHubYamlTokenSourceConfig | null {
function readGitHubYamlAuthConfig(): GitHubYamlAuthConfig {
const configPath = rootPath(UNIDESK_CLI_CONFIG_PATH);
if (!existsSync(configPath)) return null;
if (!existsSync(configPath)) return { tokenSource: null, repositoryOverrides: [] };
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 {
const repositoryOverrides = optionalArray(auth?.repositoryOverrides, GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF);
const parsedTokenSource = tokenSource === null ? null : {
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),
scope: "global" as const,
configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
};
const seenRepositories = new Set<string>();
const parsedOverrides = repositoryOverrides.map((value, index) => {
const configRef = `${GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF}[${index}]`;
const override = asRecord(value, configRef);
const repository = repositoryField(override, "repository", configRef);
if (seenRepositories.has(repository)) throw new Error(`${GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF} contains duplicate repository ${repository}`);
seenRepositories.add(repository);
return {
repository,
priority: tokenSourcePriorityField(override, "priority", configRef),
root: stringField(override, "root", configRef),
sourceRef: stringField(override, "sourceRef", configRef),
sourceKey: envKeyField(override, "sourceKey", configRef),
format: tokenSourceFormatField(override, "format", configRef),
scope: "repository-override" as const,
configRef,
};
});
return { tokenSource: parsedTokenSource, repositoryOverrides: parsedOverrides };
}
function parseYamlTokenSource(text: string, config: GitHubYamlTokenSourceConfig): Record<string, string> {
@@ -162,11 +194,6 @@ function resolveYamlSourcePath(root: string, sourceRef: string): string {
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")}`;
}
@@ -181,6 +208,12 @@ function optionalRecord(value: unknown, path: string): Record<string, unknown> |
return asRecord(value, path);
}
function optionalArray(value: unknown, path: string): unknown[] {
if (value === undefined || value === null) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be a YAML list`);
return value;
}
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`);
@@ -193,6 +226,12 @@ function envKeyField(obj: Record<string, unknown>, key: string, path: string): s
return value;
}
function repositoryField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[^/\s]+\/[^/\s]+$/u.test(value)) throw new Error(`${path}.${key} must be in owner/name format`);
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`);
+1 -1
View File
@@ -11,7 +11,7 @@ import type { GitHubCommandResult, GitHubDegradedReason, GitHubIssue, GitHubPull
export async function authStatus(repo: string): Promise<GitHubCommandResult> {
const ghPath = ghBinaryPath();
const { token, probe } = resolveToken(ghPath !== null);
const { token, probe } = resolveToken(repo, ghPath !== null);
const degraded: GitHubDegradedReason[] = [];
if (ghPath === null) degraded.push("missing-binary");
if (!probe.present || token === null) {
+1
View File
@@ -383,6 +383,7 @@ function renderAuthStatus(result: GitHubCommandResult): string {
const probes = record(result.probes);
const tokenDetail = [
`source=${ghText(token.source)}`,
`scope=${ghText(token.scope)}`,
`yaml=${ghText(token.yamlSourceEnabled)}`,
`priority=${ghText(token.yamlSourcePriority)}`,
`sourceRef=${ghText(token.sourceRef)}`,
+71 -49
View File
@@ -230,7 +230,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
});
}
if (sub === "create" && options.dryRun) return repoCreate(options.repo, "", options);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(options.repo, true);
const commandName = `repo ${sub}`;
const missing = authRequired(options.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
@@ -259,7 +259,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
}
const resolved = resolvePositionalIssueReference(args, 3, commandName, options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
if (action === "list") return withNumberOptionHint(issueAttachmentList(resolved.repo, token, resolved.number), resolved);
@@ -271,7 +271,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (isGitHubCommandResult(resolved)) return resolved;
const commentId = resolved.number;
if (typeof commentId !== "number") return commentId;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentView(resolved.repo, token, "issue", commentId, options.raw || options.full, commandName), resolved);
@@ -282,7 +282,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const commentId = resolved.number;
if (typeof commentId !== "number") return commentId;
if (options.dryRun) return withNumberOptionHint(commentDelete(resolved.repo, "", "issue", commentId, true), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "issue comment delete", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue comment delete", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentDelete(resolved.repo, token, "issue", commentId, false), resolved);
@@ -293,7 +293,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (isGitHubCommandResult(resolved)) return resolved;
const commentId = resolved.number;
if (typeof commentId !== "number") return commentId;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentPatch(resolved.repo, token, "issue", commentId, { ...options, repo: resolved.repo }, commandName), resolved);
@@ -306,7 +306,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (typeof commentId !== "number") return commentId;
const updateOptions = { ...options, repo: resolved.repo };
if (options.dryRun) return withNumberOptionHint(commentUpdate(resolved.repo, "", "issue", commentId, updateOptions, commandName), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentUpdate(resolved.repo, token, "issue", commentId, updateOptions, commandName), resolved);
@@ -317,28 +317,28 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const issueNumber = resolved.number;
if (typeof issueNumber !== "number") return issueNumber;
if (options.dryRun) return withNumberOptionHint(issueComment(resolved.repo, "", issueNumber, { ...options, repo: resolved.repo }), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "issue comment create", probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "issue comment create", { present: false, source: null, ghFallbackAttempted: true });
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue comment create", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueComment(resolved.repo, token, issueNumber, { ...options, repo: resolved.repo }), resolved);
}
if (sub === "comments") {
const resolved = resolvePositionalIssueReference(args, 2, "issue comments", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "issue comments", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue comments", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueComments(resolved.repo, token, resolved.number, options.limit, { includeFullCommentBodies: options.full || options.raw }), resolved);
}
if (sub === "scan-escape" || sub === "cleanup-plan") {
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(options.repo, true);
const commandName = sub === "cleanup-plan" ? "issue cleanup-plan" : "issue scan-escape";
const missing = authRequired(options.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return issueScanEscape(options.repo, token, options.limit, options.dryRun || sub === "cleanup-plan", commandName);
}
if (sub === "board-audit") {
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(options.repo, true);
const missing = authRequired(options.repo, "issue board-audit", probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "issue board-audit", { present: false, source: null, ghFallbackAttempted: true });
return issueBoardAudit(options.repo, token, options);
@@ -349,15 +349,20 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (action !== "list" && action !== "get" && action !== "update" && action !== "add" && action !== "move" && action !== "delete" && action !== "upsert") {
return unsupportedCommand(commandName, options.repo, "board-row supported commands are list, get, update, add, move, delete, and upsert.");
}
const { token, probe } = resolveToken(true);
const missing = authRequired(options.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
if (action === "list") return issueBoardRowList(options.repo, token, options);
if (action === "list") {
const { token, probe } = resolveToken(options.repo, true);
const missing = authRequired(options.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return issueBoardRowList(options.repo, token, options);
}
const resolvedBoardRow = resolvePositionalIssueReference(args, 3, commandName, options);
if (isGitHubCommandResult(resolvedBoardRow)) return resolvedBoardRow;
const issueNumber = resolvedBoardRow.number;
if (typeof issueNumber !== "number") return issueNumber;
const boardRowOptions = { ...options, repo: resolvedBoardRow.repo };
const { token, probe } = resolveToken(resolvedBoardRow.repo, true);
const missing = authRequired(resolvedBoardRow.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolvedBoardRow.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
if (action === "get") return withNumberOptionHint(issueBoardRowGet(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
if (action === "update") return withNumberOptionHint(issueBoardRowUpdate(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
if (action === "add") return withNumberOptionHint(issueBoardRowAdd(resolvedBoardRow.repo, token, issueNumber, boardRowOptions), resolvedBoardRow);
@@ -368,7 +373,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "patch") {
const resolved = resolvePositionalIssueReference(args, 2, "issue patch", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "issue patch", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "issue patch", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issuePatch(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
@@ -379,14 +384,14 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const issueEditRef = resolvePositionalIssueReference(args, 2, "issue edit", options);
if (isGitHubCommandResult(issueEditRef)) return issueEditRef;
const issueNumber = issueEditRef.number;
const { token } = resolveToken(false);
const { token } = resolveToken(issueEditRef.repo, false);
return withNumberOptionHint(issueEdit(issueEditRef.repo, token ?? "", issueNumber, { ...options, repo: issueEditRef.repo }), issueEditRef);
}
if (sub === "update") {
const issueUpdateRef = resolvePositionalIssueReference(args, 2, "issue update", options);
if (isGitHubCommandResult(issueUpdateRef)) return issueUpdateRef;
const issueNumber = issueUpdateRef.number;
const { token } = resolveToken(false);
const { token } = resolveToken(issueUpdateRef.repo, false);
return withNumberOptionHint(issueEdit(issueUpdateRef.repo, token ?? "", issueNumber, { ...options, repo: issueUpdateRef.repo }, "issue update"), issueUpdateRef);
}
if (sub === "comment") {
@@ -408,43 +413,60 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "read" || sub === "view") {
const resolved = resolveReadViewNumberReference("issue", sub, third, options, args);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, `issue ${sub}`, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, `issue ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
const disclosure = readDisclosureOptions(options, resolved.shorthand);
if (sub === "read") return withNumberOptionHint(issueRead(resolved.repo, token, resolved.number, issueReadJsonFields(options), "issue read", disclosure, { includeFullCommentBodies: options.raw }), resolved);
return withNumberOptionHint(issueView(resolved.repo, token, resolved.number, issueReadJsonFields(options), disclosure, { includeFullCommentBodies: options.raw }), resolved);
}
const { token, probe } = resolveToken(true);
const missing = authRequired(options.repo, `issue ${sub ?? ""}`.trim(), probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `issue ${sub ?? ""}`.trim(), { present: false, source: null, ghFallbackAttempted: true });
if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields, options.search, options.labels, options.raw || options.full, options.titlePrefix);
if (sub === "stale-close") return issueStaleClose(options.repo, token, options);
if (sub === "create") return issueCreate(options.repo, token, options);
if (sub === "list" || sub === "stale-close" || sub === "create") {
const commandName = `issue ${sub}`;
const { token, probe } = resolveToken(options.repo, true);
const missing = authRequired(options.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
if (sub === "list") return issueList(options.repo, token, options.listState, options.limit, options.issueListJsonFields, options.search, options.labels, options.raw || options.full, options.titlePrefix);
if (sub === "stale-close") return issueStaleClose(options.repo, token, options);
return issueCreate(options.repo, token, options);
}
if (sub === "edit") {
const r = resolvePositionalIssueReference(args, 2, "issue edit", options);
if (isGitHubCommandResult(r)) return r;
const { token, probe } = resolveToken(r.repo, true);
const missing = authRequired(r.repo, "issue edit", probe);
if (missing !== null || token === null) return missing ?? authRequired(r.repo, "issue edit", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueEdit(r.repo, token, r.number, { ...options, repo: r.repo }), r);
}
if (sub === "update") {
const r = resolvePositionalIssueReference(args, 2, "issue update", options);
if (isGitHubCommandResult(r)) return r;
const { token, probe } = resolveToken(r.repo, true);
const missing = authRequired(r.repo, "issue update", probe);
if (missing !== null || token === null) return missing ?? authRequired(r.repo, "issue update", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueEdit(r.repo, token, r.number, { ...options, repo: r.repo }, "issue update"), r);
}
if (sub === "comment") {
const r = resolvePositionalIssueReference(args, 2, "issue comment", options);
if (isGitHubCommandResult(r)) return r;
const { token, probe } = resolveToken(r.repo, true);
const missing = authRequired(r.repo, "issue comment", probe);
if (missing !== null || token === null) return missing ?? authRequired(r.repo, "issue comment", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueComment(r.repo, token, r.number, { ...options, repo: r.repo }), r);
}
if (sub === "close") {
const r = resolvePositionalIssueReference(args, 2, "issue close", options);
if (isGitHubCommandResult(r)) return r;
const { token, probe } = resolveToken(r.repo, true);
const missing = authRequired(r.repo, "issue close", probe);
if (missing !== null || token === null) return missing ?? authRequired(r.repo, "issue close", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueState(r.repo, token, r.number, "closed", options.dryRun, { ...options, repo: r.repo }), r);
}
if (sub === "reopen") {
const r = resolvePositionalIssueReference(args, 2, "issue reopen", options);
if (isGitHubCommandResult(r)) return r;
const { token, probe } = resolveToken(r.repo, true);
const missing = authRequired(r.repo, "issue reopen", probe);
if (missing !== null || token === null) return missing ?? authRequired(r.repo, "issue reopen", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(issueState(r.repo, token, r.number, "open", options.dryRun, { ...options, repo: r.repo }), r);
}
}
@@ -462,7 +484,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
});
}
if (optionWasProvided(args, "--file")) {
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr diff", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr diff", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prDiffFile(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
@@ -478,7 +500,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
],
});
}
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr diff --stat", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr diff --stat", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prFiles(resolved.repo, token, resolved.number, options.limit, "pr diff --stat"), resolved);
@@ -486,7 +508,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "review-plan") {
const resolved = resolvePositionalPrReference(args, 2, "pr review-plan", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr review-plan", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr review-plan", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prReviewPlan(resolved.repo, token, resolved.number, options.limit), resolved);
@@ -494,7 +516,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "files") {
const resolved = resolvePositionalPrReference(args, 2, "pr files", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr files", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr files", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prFiles(resolved.repo, token, resolved.number, options.limit, "pr files"), resolved);
@@ -509,7 +531,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const resolved = resolvePositionalNumberReference("pr", args, 3, "pr comment delete", options);
if (isGitHubCommandResult(resolved)) return resolved;
if (options.dryRun) return withNumberOptionHint(commentDelete(resolved.repo, "", "pr", resolved.number, true), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr comment delete", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr comment delete", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentDelete(resolved.repo, token, "pr", resolved.number, false), resolved);
@@ -518,7 +540,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const commandName = `pr comment ${third}`;
const resolved = resolvePositionalNumberReference("pr", args, 3, commandName, options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentView(resolved.repo, token, "pr", resolved.number, options.raw || options.full, commandName), resolved);
@@ -529,7 +551,7 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (isGitHubCommandResult(resolved)) return resolved;
const updateOptions = { ...options, repo: resolved.repo };
if (options.dryRun) return withNumberOptionHint(commentUpdate(resolved.repo, "", "pr", resolved.number, updateOptions, commandName), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(commentUpdate(resolved.repo, token, "pr", resolved.number, updateOptions, commandName), resolved);
@@ -538,14 +560,14 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const resolved = resolvePositionalPrReference(args, 3, "pr comment create", options);
if (isGitHubCommandResult(resolved)) return resolved;
if (options.dryRun) return withNumberOptionHint(prComment(resolved.repo, "", resolved.number, { ...options, repo: resolved.repo }), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr comment create", probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr comment create", { present: false, source: null, ghFallbackAttempted: true });
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr comment create", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prComment(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
}
if (sub === "create") {
if (options.dryRun) return prCreate(options.repo, "", options);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(options.repo, true);
const missing = authRequired(options.repo, "pr create", probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr create", { present: false, source: null, ghFallbackAttempted: true });
return prCreate(options.repo, token, options);
@@ -556,11 +578,11 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (isGitHubCommandResult(resolved)) return resolved;
return withNumberOptionHint(prComment(resolved.repo, "", resolved.number, { ...options, repo: resolved.repo }), resolved);
}
const { token, probe } = resolveToken(true);
const missing = authRequired(options.repo, "pr comment", probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr comment", { present: false, source: null, ghFallbackAttempted: true });
const resolved = resolvePositionalPrReference(args, 2, "pr comment", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr comment", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr comment", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prComment(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }), resolved);
}
if (sub === "update" || sub === "edit") {
@@ -568,24 +590,24 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const resolved = resolvePositionalPrReference(args, 2, commandName, options);
if (isGitHubCommandResult(resolved)) return resolved;
if (options.dryRun && options.mode === "replace") return withNumberOptionHint(prUpdate(resolved.repo, "", resolved.number, { ...options, repo: resolved.repo }, commandName), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, commandName, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prUpdate(resolved.repo, token, resolved.number, { ...options, repo: resolved.repo }, commandName), resolved);
}
if (sub === "close" || sub === "reopen") {
const resolved = resolvePositionalPrReference(args, 2, `pr ${sub}`, options);
if (isGitHubCommandResult(resolved)) return resolved;
if (options.dryRun) return withNumberOptionHint(prState(resolved.repo, "", resolved.number, sub === "close" ? "closed" : "open", true), resolved);
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, `pr ${sub}`, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prState(resolved.repo, token, resolved.number, sub === "close" ? "closed" : "open", false), resolved);
}
if (sub === "ready") {
const resolved = resolvePositionalPrReference(args, 2, "pr ready", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr ready", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr ready", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prReady(resolved.repo, token, resolved.number, options.dryRun), resolved);
@@ -593,9 +615,9 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "merge") {
const resolved = resolvePositionalPrReference(args, 2, "pr merge", options);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const missing = authRequired(options.repo, "pr merge", probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, "pr merge", { present: false, source: null, ghFallbackAttempted: true });
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, "pr merge", probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, "pr merge", { present: false, source: null, ghFallbackAttempted: true });
return withNumberOptionHint(prMerge(resolved.repo, token, resolved.number, options), resolved);
}
if (sub !== "list" && !isPrReadCommand(sub)) {
@@ -604,14 +626,14 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
if (sub === "read" || sub === "view") {
const resolved = resolveReadViewNumberReference("pr", sub, third, options, args);
if (isGitHubCommandResult(resolved)) return resolved;
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(resolved.repo, true);
const missing = authRequired(resolved.repo, `pr ${sub}`, probe);
if (missing !== null || token === null) return missing ?? authRequired(resolved.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
const disclosure = readDisclosureOptions(options, resolved.shorthand);
if (sub === "read") return withNumberOptionHint(prRead(resolved.repo, token, resolved.number, prReadJsonFields(options), "pr read", disclosure), resolved);
return withNumberOptionHint(prView(resolved.repo, token, resolved.number, prReadJsonFields(options), disclosure), resolved);
}
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(options.repo, true);
const missing = authRequired(options.repo, `pr ${sub}`, probe);
if (missing !== null || token === null) return missing ?? authRequired(options.repo, `pr ${sub}`, { present: false, source: null, ghFallbackAttempted: true });
if (sub === "list") return prList(options.repo, token, options.prListState, options.limit, options.prListJsonFields, options.raw || options.full);
+1 -1
View File
@@ -239,7 +239,7 @@ export async function prPreflight(repo: string, number: number, commandName: "pr
details: auth,
};
}
const { token, probe } = resolveToken(true);
const { token, probe } = resolveToken(repo, true);
const missing = authRequired(repo, commandName, probe);
if (missing !== null || token === null) {
const missingResult = missing ?? authRequired(repo, commandName, { present: false, source: null, ghFallbackAttempted: true });
+1 -1
View File
@@ -411,6 +411,7 @@ export type GitHubCommandFailure = GitHubCommandResult & { ok: false };
export interface GitHubTokenProbe {
present: boolean;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "yaml-token-source" | "gh-auth-token" | null;
scope?: "repository-override" | "global";
ghFallbackAttempted: boolean;
ghBinaryFound?: boolean;
ghAuthTokenAvailable?: boolean | null;
@@ -420,7 +421,6 @@ export interface GitHubTokenProbe {
configRef?: string;
sourceRef?: string;
sourceKey?: string;
sourcePath?: string;
sourceExists?: boolean;
sourceKeyPresent?: boolean;
tokenFingerprint?: string | null;