diff --git a/docs/MDTODO/details/selfmedia-production-delivery/R1.1_Task_Report.md b/docs/MDTODO/details/selfmedia-production-delivery/R1.1_Task_Report.md index 6c627635..98de4f55 100644 --- a/docs/MDTODO/details/selfmedia-production-delivery/R1.1_Task_Report.md +++ b/docs/MDTODO/details/selfmedia-production-delivery/R1.1_Task_Report.md @@ -32,12 +32,12 @@ - `bun test scripts/src/platform-infra-gitea-config.test.ts scripts/src/platform-infra-gitea-disclosure.test.ts`:11 项通过,0 项失败。 - `bun scripts/cli.ts platform-infra gitea plan --target NC01`:policy 为 `ok`。 - `git diff --check`、Node 语法、Python `py_compile`、Shell `sh -n`:通过。 -- `mirror bootstrap --target NC01 --repo selfmedia-nc01 --dry-run`:`OK=false`,blocker 为 `github-upstream:selfmedia-nc01`,符合 fail-closed。 -- `mirror webhook status --target NC01 --repo selfmedia-nc01 --full`:在本地 credential blocker 处早返回,包含 `sourceRef`、`present=false`、`fingerprint=null`、`permissionResult=blocked-missing-credential` 与有界错误码;未访问运行面,JSON/stderr 均无 stack 或绝对 worktree 路径。 -- 默认 `mirror webhook status` compact 输出使用短表,按 `error.blockers` 精确匹配每个 credential/repository;带或不带 `--repo` 均只显示正确的 selfmedia blocker,`--full`/`--raw` 保持结构化。 +- `mirror bootstrap --target NC01 --repo selfmedia-nc01 --dry-run --full`:`OK=true`、`blockers=[]`;全局与 repository override 均只显示脱敏 presence/fingerprint,不显示 token。 +- `mirror webhook status --target NC01 --repo selfmedia-nc01 --full`:可读取私有仓库 HEAD `6e6a95297c3c153ab269410a079a997adc345774`;当前 `hookReady=false`、`driftTypes=[desired-url-missing]`、有界错误为 `github-hook-missing`。 +- 三个 missing-credential 回归改用 synthetic blocker 输入,不再 spawn 宿主 CLI、不读取 sourceRef、不接触或打印 token;compact/full 合同仍覆盖。 - `mirror bootstrap --target JD01 --dry-run`:`OK=true`,证明 JD01 不依赖 NC01 override。 -- 专用凭据只读状态:`sourceRef=pikainc-selfmedia-gh-token.txt`、`presence=false`、`fingerprint=null`、`permissionResult=blocked-missing-credential`。 -- 因 credential material 缺失,未执行权限 API 探测;不得声称 Contents、Metadata 或 Webhooks 权限已实际授予。 +- 专用凭据只读状态:`sourceRef=pikainc-selfmedia-gh-token.txt`、`presence=true`、`mode=600`、`fingerprint=sha256:5eed1c04a44ac5e4`、`valuesPrinted=false`。 +- 受控 status 已验证私有 HEAD 与 hooks list 可读,但未创建 hook;不得把可读性推断为超出 YAML 声明的额外权限,也不得披露 token 值。 ## 并行保护 @@ -62,5 +62,5 @@ ## 当前结论 - R1.1 配置、schema、renderer、Secret、bridge、observer 与受控 fetch 的源码能力已落地并通过轻量验证。 -- 外部 credential material 仍缺失,R1.1 保持 `in_progress`。 -- 凭据到位后的下一步仅允许通过受控 CLI 观察脱敏 permissionResult、hook reconciler 与自动交付收敛,不得以手工触发替代自动链路。 +- repository-scoped credential material 已到位且 mode 为 `0600`;当前 GitHub hook 尚未创建,R1.1 保持 `in_progress`。 +- 下一步仅允许在授权的自动控制面中观察 hook reconciler 与自动交付收敛;本次未执行 hook 创建、Secret sync、apply、bootstrap 或任何部署。 diff --git a/scripts/src/platform-infra-gitea-config.test.ts b/scripts/src/platform-infra-gitea-config.test.ts index e141026d..eb512873 100644 --- a/scripts/src/platform-infra-gitea-config.test.ts +++ b/scripts/src/platform-infra-gitea-config.test.ts @@ -4,7 +4,34 @@ import { readGiteaConfig, resolveTarget, } from "./platform-infra-gitea-config"; -import { renderManifest } from "./platform-infra-gitea"; +import { buildMirrorWebhookCredentialBlockedResult, renderManifest } from "./platform-infra-gitea"; +import { renderMirrorWebhookCredentialBlocked } from "./platform-infra-gitea-render"; + +function syntheticMissingSelfmediaCredential(): Record { + return { + id: "github-upstream:selfmedia-nc01", + repository: "pikainc/selfmedia", + sourceRef: "pikainc-selfmedia-gh-token.txt", + present: false, + requiredKeysPresent: false, + fingerprint: null, + permissionResult: { + status: "blocked-missing-credential", + permissions: { contents: "read", metadata: "read", webhooks: "read-write" }, + error: "credential material is absent", + }, + valuesPrinted: false, + }; +} + +function syntheticBlockedResult(allTargetRepositories = false): Record { + const config = readGiteaConfig(); + const target = resolveTarget(config, "NC01"); + const repositories = config.sourceAuthority.repositories.filter((repo) => repo.targetId === "NC01" + && (allTargetRepositories || repo.key === "selfmedia-nc01")); + const credential = syntheticMissingSelfmediaCredential(); + return buildMirrorWebhookCredentialBlockedResult(config, target, repositories, [credential], [String(credential.id)]); +} describe("platform-infra Gitea repository GitHub credentials", () => { test("keeps the global credential and selfmedia override on distinct Secret keys", () => { @@ -40,39 +67,25 @@ describe("platform-infra Gitea repository GitHub credentials", () => { test("does not inject the NC01 selfmedia token into JD01 bridge containers", () => { const config = readGiteaConfig(); - const tokenEnv = githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia"); + const selfmediaTokenEnv = githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia"); + const globalTokenEnv = githubTokenEnvNameForSecretKey(config.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key); const jd01Manifest = renderManifest(config, resolveTarget(config, "JD01")); const nc01Manifest = renderManifest(config, resolveTarget(config, "NC01")); - expect(jd01Manifest).not.toContain(tokenEnv); - expect(nc01Manifest).toContain(tokenEnv); + expect(jd01Manifest).toContain(globalTokenEnv); + expect(jd01Manifest).not.toContain(selfmediaTokenEnv); + expect(nc01Manifest).toContain(globalTokenEnv); + expect(nc01Manifest).toContain(selfmediaTokenEnv); + expect(nc01Manifest).toContain(`"tokenEnv": "${selfmediaTokenEnv}"`); }); test("keeps missing selfmedia webhook status bounded and stack-free", () => { - const result = Bun.spawnSync({ - cmd: [ - "bun", - "scripts/cli.ts", - "platform-infra", - "gitea", - "mirror", - "webhook", - "status", - "--target", - "NC01", - "--repo", - "selfmedia-nc01", - "--full", - ], - stdout: "pipe", - stderr: "pipe", - }); - const stdout = result.stdout.toString(); - const payload = JSON.parse(stdout); - const credentials = payload.data.credentials as Array>; - const selfmedia = credentials.find((item) => item.id === "github-upstream:selfmedia-nc01"); + const payload = syntheticBlockedResult(); + const credentials = payload.credentials as Array>; + const selfmedia = credentials[0]; + const serialized = JSON.stringify(payload); - expect(payload.data.error.code).toBe("github-credential-unavailable"); + expect((payload.error as Record).code).toBe("github-credential-unavailable"); expect(selfmedia).toMatchObject({ sourceRef: "pikainc-selfmedia-gh-token.txt", present: false, @@ -80,29 +93,12 @@ describe("platform-infra Gitea repository GitHub credentials", () => { valuesPrinted: false, }); expect(selfmedia).not.toHaveProperty("sourcePath"); - expect(stdout).not.toContain('"stack"'); - expect(result.stderr.toString()).not.toContain("platform-infra-gitea.ts:"); + expect(serialized).not.toContain('"stack"'); + expect(serialized).not.toContain("/root/.worktrees/"); }); test("renders missing selfmedia webhook status as compact text by default", () => { - const result = Bun.spawnSync({ - cmd: [ - "bun", - "scripts/cli.ts", - "platform-infra", - "gitea", - "mirror", - "webhook", - "status", - "--target", - "NC01", - "--repo", - "selfmedia-nc01", - ], - stdout: "pipe", - stderr: "pipe", - }); - const stdout = result.stdout.toString(); + const stdout = renderMirrorWebhookCredentialBlocked(syntheticBlockedResult()).renderedText; expect(stdout).toContain("PLATFORM-INFRA GITEA MIRROR WEBHOOK STATUS"); expect(stdout).toContain("SOURCE_REF"); @@ -111,26 +107,10 @@ describe("platform-infra Gitea repository GitHub credentials", () => { expect(stdout).toContain("github-upstream:selfmedia-nc01"); expect(stdout.trimStart()).not.toStartWith("{"); expect(stdout).not.toContain('"stack"'); - expect(result.stderr.toString()).not.toContain("platform-infra-gitea.ts:"); }); test("matches compact blocker rows to their repository without --repo", () => { - const result = Bun.spawnSync({ - cmd: [ - "bun", - "scripts/cli.ts", - "platform-infra", - "gitea", - "mirror", - "webhook", - "status", - "--target", - "NC01", - ], - stdout: "pipe", - stderr: "pipe", - }); - const stdout = result.stdout.toString(); + const stdout = renderMirrorWebhookCredentialBlocked(syntheticBlockedResult(true)).renderedText; expect(stdout).toContain("pikainc/selfmedia"); expect(stdout).toContain("pikainc-selfmedia-gh-token.txt"); diff --git a/scripts/src/platform-infra-gitea.ts b/scripts/src/platform-infra-gitea.ts index 03606bc9..7b21bc97 100644 --- a/scripts/src/platform-infra-gitea.ts +++ b/scripts/src/platform-infra-gitea.ts @@ -475,20 +475,7 @@ async function mirrorWebhookStatus(config: UniDeskConfig, options: MirrorWebhook const repos = selectedRepositories(gitea, target, options.repoKey); const credentials = credentialSummaries(gitea, repos); const blockers = credentialBlockers(credentials); - if (blockers.length > 0) { - return { - ok: false, - action: "platform-infra-gitea-mirror-webhook-status", - mutation: false, - credentialBlocked: true, - target: targetSummary(target), - webhook: webhookSyncSummary(gitea, target), - repositories: repos.map((repo) => repositorySummary(gitea, repo)), - credentials, - error: { code: "github-credential-unavailable", blockers, valuesPrinted: false }, - next: mirrorReadOnlyNextCommands(target.id), - }; - } + if (blockers.length > 0) return buildMirrorWebhookCredentialBlockedResult(gitea, target, repos, credentials, blockers); const secrets = ensureMirrorSecrets(gitea, target, repos, false, false, false); const result = await capture(config, target.route, ["sh"], remoteScript("mirror-webhook-status", gitea, target, "", { ...options, confirm: false, dryRun: true, wait: false }, { repos, secrets }).script); const parsed = parseJsonOutput(result.stdout); @@ -508,6 +495,27 @@ async function mirrorWebhookStatus(config: UniDeskConfig, options: MirrorWebhook }; } +export function buildMirrorWebhookCredentialBlockedResult( + gitea: GiteaConfig, + target: GiteaTarget, + repos: GiteaMirrorRepository[], + credentials: Array>, + blockers: string[], +): Record { + return { + ok: false, + action: "platform-infra-gitea-mirror-webhook-status", + mutation: false, + credentialBlocked: true, + target: targetSummary(target), + webhook: webhookSyncSummary(gitea, target), + repositories: repos.map((repo) => repositorySummary(gitea, repo)), + credentials, + error: { code: "github-credential-unavailable", blockers, valuesPrinted: false }, + next: mirrorReadOnlyNextCommands(target.id), + }; +} + export function renderManifest(gitea: GiteaConfig, target: GiteaTarget): string { const app = gitea.app; const image = `${app.image.repository}:${app.image.tag}`;