fix: 收窄 PaC bootstrap 预检凭据

This commit is contained in:
Codex
2026-07-13 21:05:04 +02:00
parent 697b6933fe
commit 1a04b02985
4 changed files with 71 additions and 9 deletions
@@ -4,7 +4,7 @@ import {
readGiteaConfig,
resolveTarget,
} from "./platform-infra-gitea-config";
import { buildMirrorWebhookCredentialBlockedResult, renderManifest } from "./platform-infra-gitea";
import { buildGiteaMirrorBootstrapCredentialPreflight, buildMirrorWebhookCredentialBlockedResult, renderManifest } from "./platform-infra-gitea";
import { renderMirrorWebhookCredentialBlocked } from "./platform-infra-gitea-render";
function syntheticMissingSelfmediaCredential(): Record<string, unknown> {
@@ -65,6 +65,28 @@ describe("platform-infra Gitea repository GitHub credentials", () => {
expect(nc01Overrides.map((credential) => credential.sourceRef)).toContain("pikainc-selfmedia-gh-token.txt");
});
test("bootstrap preflight only requires the selected repository effective credential", () => {
const config = readGiteaConfig();
const repository = config.sourceAuthority.repositories.find((repo) => repo.key === "selfmedia-nc01");
expect(repository).toBeDefined();
const result = buildGiteaMirrorBootstrapCredentialPreflight(config, [repository!], (credential) => (
credential.sourceRef === "pikainc-selfmedia-gh-token.txt"
? { [credential.sourceKey]: "fixture-token" }
: null
));
expect(result.blockers).toEqual([]);
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).toMatchObject({
id: "github-upstream:selfmedia-nc01",
sourceRef: "pikainc-selfmedia-gh-token.txt",
present: true,
requiredKeysPresent: true,
});
expect(JSON.stringify(result)).not.toContain(config.sourceAuthority.credentials.admin.sourceRef);
expect(JSON.stringify(result)).not.toContain(config.sourceAuthority.credentials.github.sourceRef);
});
test("does not inject the NC01 selfmedia token into JD01 bridge containers", () => {
const config = readGiteaConfig();
const selfmediaTokenEnv = githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia");
+32 -2
View File
@@ -131,8 +131,7 @@ export async function runGiteaMirrorBootstrapPreflight(
const gitea = readGiteaConfig();
const target = resolveCommandTarget(gitea, targetId);
const repos = selectedRepositories(gitea, target, repoKey);
const credentials = credentialSummaries(gitea, repos);
const blockers = credentialBlockers(credentials);
const { credentials, blockers } = buildGiteaMirrorBootstrapCredentialPreflight(gitea, repos);
if (blockers.length > 0) {
return { ok: false, mutation: false, code: "github-credential-unavailable", blockers, repositories: repos.map((repo) => repositorySummary(gitea, repo)), valuesPrinted: false };
}
@@ -142,6 +141,37 @@ export async function runGiteaMirrorBootstrapPreflight(
return parsed ?? { ok: false, mutation: false, code: "github-upstream-preflight-failed", remote: compactCapture(result, { full: true }), valuesPrinted: false };
}
export function buildGiteaMirrorBootstrapCredentialPreflight(
gitea: GiteaConfig,
repositories: GiteaMirrorRepository[],
readCredential: (credential: GiteaGithubCredential) => Record<string, string> | null = (credential) => readGithubCredential(gitea, credential),
): { credentials: Array<Record<string, unknown>>; blockers: string[] } {
const credentials = repositories.map((repo) => {
const github = githubCredentialForRepo(gitea, repo);
const values = readCredential(github);
const present = values !== null;
const requiredKeysPresent = Boolean(values?.[github.sourceKey]);
return {
id: `github-upstream:${repo.key}`,
repository: repo.upstream.repository,
sourceRef: github.sourceRef,
present,
requiredKeysPresent,
fingerprint: present ? fingerprintKeys(values, [github.sourceKey]) : null,
permissionResult: present && requiredKeysPresent
? { status: "not-probed", permissions: github.permissions, error: null }
: { status: "blocked-missing-credential", permissions: github.permissions, error: "credential material is absent" },
valuesPrinted: false,
};
});
return { credentials, blockers: credentialBlockers(credentials) };
}
function readGithubCredential(gitea: GiteaConfig, github: GiteaGithubCredential): Record<string, string> | null {
const sourcePath = credentialPath(gitea, github.sourceRef);
return existsSync(sourcePath) ? parseGithubCredentialFile(sourcePath, github) : null;
}
function mirrorPreflightSecrets(gitea: GiteaConfig, repositories: GiteaMirrorRepository[]): MirrorSecrets {
const githubTokens: Record<string, string> = {};
for (const repo of repositories) {
@@ -103,12 +103,21 @@ test("PaC bootstrap projects typed stages and compact JSON", () => {
expect(rendered).toContain("confirm:");
});
test("PaC bootstrap returns typed YAML match failures without mutation", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: 2 exact matches"));
test("PaC bootstrap returns fix-yaml next for zero YAML matches", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: no exact match"));
expect(projectPacBootstrapResult(result)).toBe(result);
expect(result.ok).toBe(false);
expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-no-match");
expect(result.next).toEqual({
kind: "fix-yaml",
command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配",
});
});
test("PaC bootstrap returns fix-yaml next for multiple YAML matches", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: 2 exact matches"));
expect(result.mutation).toBe(false);
expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-multiple-matches");
expect((result.next as Record<string, unknown>).kind).toBe("fix-yaml");
});
test("PaC bootstrap distinguishes GitHub access failure from Gitea and PaC stages", () => {
@@ -36,7 +36,6 @@ export function pacBootstrapYamlMatchFailure(
): Record<string, unknown> {
const reason = error instanceof Error ? error.message : String(error);
const matchCount = reason.includes("no exact match") ? 0 : Number.parseInt(reason.match(/(\d+) exact matches/u)?.[1] ?? "-1", 10);
const status = `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId} --consumer ${consumerId}`;
return {
ok: false,
action: "platform-infra-pipelines-as-code-bootstrap",
@@ -49,7 +48,10 @@ export function pacBootstrapYamlMatchFailure(
],
stages: [],
blockers: [{ code: matchCount === 0 ? "yaml-repository-no-match" : "yaml-repository-multiple-matches", stage: "yaml-exact-match", reason }],
next: { command: status, kind: "read-only-status" },
next: {
kind: "fix-yaml",
command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配",
},
valuesPrinted: false,
};
}
@@ -65,7 +67,6 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
const gitea = record(result.giteaBootstrap);
const apply = record(result.pipelinesAsCodeApply);
const remote = record(apply.remote);
const preflight = record(remote.preflight);
const blockers = bootstrapBlockers(githubPreflight, gitea, apply, remote);
const dryRun = result.mode === "dry-run";
const repositoryMissing = remote.error === "gitea-repository-missing";