Merge pull request #2020 from pikasTech/fix/agentrun-private-pr-auth
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix: 投影 Artificer 私有仓 GitHub 凭据
This commit is contained in:
Lyon
2026-07-14 19:25:30 +08:00
committed by GitHub
6 changed files with 106 additions and 10 deletions
@@ -2,11 +2,14 @@ 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 { authRequired } from "./gh/client";
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 projectedEnv = "UNIDESK_TEST_GH_REPOSITORY_OVERRIDE";
const originalProjectedToken = process.env[projectedEnv];
const temporaryRoots: string[] = [];
afterEach(() => {
@@ -14,6 +17,8 @@ afterEach(() => {
else process.env.GH_TOKEN = originalGhToken;
if (originalGithubToken === undefined) delete process.env.GITHUB_TOKEN;
else process.env.GITHUB_TOKEN = originalGithubToken;
if (originalProjectedToken === undefined) delete process.env[projectedEnv];
else process.env[projectedEnv] = originalProjectedToken;
while (temporaryRoots.length > 0) rmSync(temporaryRoots.pop()!, { recursive: true, force: true });
});
@@ -84,6 +89,67 @@ describe("GitHub repository token overrides", () => {
expect(selected.probe).toMatchObject({ source: "yaml-token-source", scope: "repository-override", yamlSourcePriority: "before-env" });
});
test("uses the YAML-declared projected environment for repository overrides", () => {
process.env.GH_TOKEN = "global-environment-token";
process.env[projectedEnv] = "projected-repository-token";
const authConfig = testAuthConfig();
authConfig.repositoryOverrides[0] = {
...authConfig.repositoryOverrides[0],
sourceRef: "missing-token.txt",
projectedEnv,
};
const selected = resolveToken("pikainc/selfmedia", false, authConfig);
expect(selected.token).toBe("projected-repository-token");
expect(selected.probe).toMatchObject({
source: "yaml-token-projection",
scope: "repository-override",
sourceExists: false,
sourceKeyPresent: false,
projectedEnv,
projectedEnvPresent: true,
valuesPrinted: false,
});
});
test("fails closed when a declared repository override is missing", () => {
process.env.GH_TOKEN = "global-environment-token";
delete process.env[projectedEnv];
const authConfig = testAuthConfig();
authConfig.repositoryOverrides[0] = {
...authConfig.repositoryOverrides[0],
sourceRef: "missing-token.txt",
projectedEnv,
};
const selected = resolveToken("pikainc/selfmedia", true, authConfig);
const failure = authRequired("pikainc/selfmedia", "pr view", selected.probe);
expect(selected.token).toBeNull();
expect(selected.probe).toMatchObject({
present: false,
scope: "repository-override",
sourceExists: false,
sourceKeyPresent: false,
projectedEnv,
projectedEnvPresent: false,
ghFallbackAttempted: false,
valuesPrinted: false,
});
expect(failure).toMatchObject({
ok: false,
degradedReason: "missing-token",
runnerDisposition: "infra-blocked",
token: {
scope: "repository-override",
projectedEnvPresent: false,
valuesPrinted: false,
},
});
expect(JSON.stringify(failure)).not.toContain("global-environment-token");
});
test("matches repository overrides without case sensitivity", () => {
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
+10 -3
View File
@@ -21,6 +21,7 @@ export interface GitHubYamlTokenSourceConfig {
sourceRef: string;
sourceKey: string;
format: "env" | "raw-token";
projectedEnv?: string;
scope: "repository-override" | "global";
configRef: string;
repository?: string;
@@ -88,13 +89,16 @@ export function tokenFromYamlSource(repo: string, authConfig: GitHubYamlAuthConf
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 sourceToken = values[config.sourceKey] ?? null;
const projectedToken = config.projectedEnv === undefined ? null : process.env[config.projectedEnv] ?? null;
const token = sourceToken ?? projectedToken;
const present = token !== null && token.length > 0;
const projectedEnvPresent = projectedToken !== null && projectedToken.length > 0;
return {
token: present ? token : null,
probe: {
present,
source: present ? "yaml-token-source" : null,
source: sourceToken !== null ? "yaml-token-source" : projectedEnvPresent ? "yaml-token-projection" : null,
scope: config.scope,
ghFallbackAttempted: false,
yamlSourceAttempted: true,
@@ -104,7 +108,8 @@ export function tokenFromYamlSource(repo: string, authConfig: GitHubYamlAuthConf
sourceRef: config.sourceRef,
sourceKey: config.sourceKey,
sourceExists,
sourceKeyPresent: present,
sourceKeyPresent: sourceToken !== null && sourceToken.length > 0,
...(config.projectedEnv === undefined ? {} : { projectedEnv: config.projectedEnv, projectedEnvPresent }),
tokenFingerprint: present ? fingerprintToken(token) : null,
valuesPrinted: false,
},
@@ -132,6 +137,7 @@ export function ghAuthToken(): string | null {
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;
if (yamlToken.probe.scope === "repository-override" && !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;
@@ -179,6 +185,7 @@ function readGitHubYamlAuthConfig(): GitHubYamlAuthConfig {
sourceRef: stringField(override, "sourceRef", configRef),
sourceKey: envKeyField(override, "sourceKey", configRef),
format: tokenSourceFormatField(override, "format", configRef),
...(override.projectedEnv === undefined ? {} : { projectedEnv: envKeyField(override, "projectedEnv", configRef) }),
scope: "repository-override" as const,
configRef,
};
+2
View File
@@ -388,6 +388,8 @@ function renderAuthStatus(result: GitHubCommandResult): string {
`priority=${ghText(token.yamlSourcePriority)}`,
`sourceRef=${ghText(token.sourceRef)}`,
`key=${ghText(token.sourceKey)}`,
`projectedEnv=${ghText(token.projectedEnv)}`,
`projectedEnvPresent=${ghText(token.projectedEnvPresent)}`,
`fingerprint=${ghText(token.tokenFingerprint)}`,
`ghFallback=${ghText(token.ghFallbackAttempted)}`,
].filter((item) => !item.endsWith("=-") && !item.endsWith("=undefined")).join(" ");
+3 -1
View File
@@ -410,7 +410,7 @@ export type GitHubCommandFailure = GitHubCommandResult & { ok: false };
export interface GitHubTokenProbe {
present: boolean;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "yaml-token-source" | "gh-auth-token" | null;
source: "GH_TOKEN" | "GITHUB_TOKEN" | "yaml-token-source" | "yaml-token-projection" | "gh-auth-token" | null;
scope?: "repository-override" | "global";
ghFallbackAttempted: boolean;
ghBinaryFound?: boolean;
@@ -423,6 +423,8 @@ export interface GitHubTokenProbe {
sourceKey?: string;
sourceExists?: boolean;
sourceKeyPresent?: boolean;
projectedEnv?: string;
projectedEnvPresent?: boolean;
tokenFingerprint?: string | null;
valuesPrinted?: false;
}