diff --git a/scripts/src/platform-infra-gitea-config.test.ts b/scripts/src/platform-infra-gitea-config.test.ts index eb512873..be9f6171 100644 --- a/scripts/src/platform-infra-gitea-config.test.ts +++ b/scripts/src/platform-infra-gitea-config.test.ts @@ -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 { @@ -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"); diff --git a/scripts/src/platform-infra-gitea.ts b/scripts/src/platform-infra-gitea.ts index b737b545..2e85410c 100644 --- a/scripts/src/platform-infra-gitea.ts +++ b/scripts/src/platform-infra-gitea.ts @@ -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 | null = (credential) => readGithubCredential(gitea, credential), +): { credentials: Array>; 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 | null { + const sourcePath = credentialPath(gitea, github.sourceRef); + return existsSync(sourcePath) ? parseGithubCredentialFile(sourcePath, github) : null; +} + function mirrorPreflightSecrets(gitea: GiteaConfig, repositories: GiteaMirrorRepository[]): MirrorSecrets { const githubTokens: Record = {}; for (const repo of repositories) { diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts index 6a00d504..940e6e55 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -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[])[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[])[0]?.code).toBe("yaml-repository-multiple-matches"); + expect((result.next as Record).kind).toBe("fix-yaml"); }); test("PaC bootstrap distinguishes GitHub access failure from Gitea and PaC stages", () => { diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts index 053c0b05..17419c52 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.ts @@ -36,7 +36,6 @@ export function pacBootstrapYamlMatchFailure( ): Record { 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): 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";