# 上游部署模板:经审阅复制到 config-center 的 .github/workflows/publish-runtime.yml。 # 需先提交独立受审工具及 metadata;本模板不是本地执行入口,也不代表已经持续上线。 name: Publish Runtime Data on: workflow_run: workflows: [Schema Validation] types: [completed] branches: [main] permissions: contents: read concurrency: group: config-center-runtime-publication cancel-in-progress: false jobs: verify: if: >- github.repository == 'desirecore/config-center' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.head_repository.id == github.event.repository.id && github.event.workflow_run.head_repository.full_name == github.repository runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read actions: read outputs: eligible: ${{ steps.gate.outputs.eligible }} source_sha: ${{ steps.gate.outputs.source_sha }} steps: - name: 固定 Node 工具链 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '24' - name: 复核同仓库精确校验运行与必需 job id: gate env: GH_TOKEN: ${{ github.token }} shell: bash run: | node <<'NODE' const fs = require('node:fs') ;(async () => { const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8')) const expected = 'desirecore/config-center', runEvent = event.workflow_run const sha = runEvent?.head_sha if (process.env.GITHUB_REPOSITORY !== expected || event.repository?.full_name !== expected || runEvent?.head_repository?.id !== event.repository?.id || runEvent?.head_repository?.full_name !== expected || runEvent.event !== 'push' || runEvent.head_branch !== 'main' || runEvent.conclusion !== 'success' || !/^[a-f0-9]{40}$/.test(sha ?? '') || !Number.isSafeInteger(runEvent.id) || !Number.isSafeInteger(runEvent.run_attempt) || runEvent.run_attempt < 1) throw new Error('guard') const request = async path => { const response = await fetch(`https://api.github.com/repos/${expected}${path}`, { headers: { accept: 'application/vnd.github+json', authorization: `Bearer ${process.env.GH_TOKEN}`, 'x-github-api-version': '2022-11-28' }, signal: AbortSignal.timeout(20000), }) if (!response.ok) throw new Error('api') return response.json() } const run = await request(`/actions/runs/${runEvent.id}`) if (run.id !== runEvent.id || run.run_attempt !== runEvent.run_attempt || run.head_sha !== sha || run.repository?.id !== event.repository.id || run.head_repository?.id !== event.repository.id || run.event !== 'push' || run.head_branch !== 'main' || run.status !== 'completed' || run.conclusion !== 'success' || run.name !== 'Schema Validation' || !Number.isSafeInteger(run.workflow_id) || run.workflow_id !== runEvent.workflow_id) throw new Error('run') const workflow = await request(`/actions/workflows/${run.workflow_id}`) if (workflow.id !== run.workflow_id || workflow.path !== '.github/workflows/validate.yml' || workflow.name !== 'Schema Validation') throw new Error('workflow') const jobs = await request(`/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100`) if (!Array.isArray(jobs.jobs) || !Number.isSafeInteger(jobs.total_count) || jobs.jobs.length !== jobs.total_count) throw new Error('jobs') const required = jobs.jobs.filter(job => job.name === 'Validate against frozen schema') if (required.length !== 1 || required[0].run_id !== run.id || required[0].head_sha !== sha || required[0].run_attempt !== run.run_attempt || required[0].status !== 'completed' || required[0].conclusion !== 'success') throw new Error('required_job') for (const name of ['Run validator', 'Run unit tests']) { const steps = required[0].steps?.filter(step => step.name === name) if (steps?.length !== 1 || steps[0].status !== 'completed' || steps[0].conclusion !== 'success') throw new Error('required_step') } const main = await request('/git/ref/heads/main') const eligible = main.object?.sha === sha fs.appendFileSync(process.env.GITHUB_OUTPUT, `eligible=${eligible}\nsource_sha=${sha}\n`) console.log(eligible ? '精确 main 校验已通过;允许纯运行派生。' : 'main 已前进;跳过旧校验运行,等待新 main 校验。') })().catch(() => { console.error('config_center_runtime_ci_verification_failed'); process.exitCode = 1 }) NODE publish: needs: verify if: needs.verify.outputs.eligible == 'true' runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write steps: - name: 读取可信发布工具(不是触发运行的 artifact) uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ github.workflow_sha }} path: runtime-tool persist-credentials: false fetch-depth: 1 sparse-checkout: .github/runtime-publication - name: 读取已验证的精确来源快照 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: ref: ${{ needs.verify.outputs.source_sha }} path: runtime-source persist-credentials: false fetch-depth: 1 submodules: false - name: 固定 Node 工具链 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: '24' - name: 审核工具摘要并派生独立本地 runtime env: SOURCE_SHA: ${{ needs.verify.outputs.source_sha }} shell: bash run: | node <<'NODE' const fs = require('node:fs'), path = require('node:path'), os = require('node:os') const crypto = require('node:crypto'), cp = require('node:child_process') function main() { const sha1 = /^[a-f0-9]{40}$/, sha256 = /^[a-f0-9]{64}$/ if (!sha1.test(process.env.SOURCE_SHA ?? '')) throw new Error('source') const toolRoot = path.join(process.env.GITHUB_WORKSPACE, 'runtime-tool', '.github', 'runtime-publication') const tool = path.join(toolRoot, 'publisher.cjs'), metadataFile = path.join(toolRoot, 'publisher.metadata.json') for (const file of [tool, metadataFile]) { const info = fs.lstatSync(file) if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size < 1 || info.size > 8 * 1024 * 1024) throw new Error('tool') } const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf8')) const builtin = new Set(require('node:module').builtinModules.map(name => name.replace(/^node:/, ''))) if (metadata.formatVersion !== 1 || metadata.profile !== 'config-center-runtime-publisher-node-cjs-v1' || !sha1.test(metadata.sourceCommit ?? '') || !sha256.test(metadata.sha256 ?? '') || !Array.isArray(metadata.inputs) || metadata.inputs.length < 1 || metadata.inputs.length > 2048 || !Array.isArray(metadata.externalImports) || metadata.externalImports.some(name => typeof name !== 'string' || !builtin.has(name.replace(/^node:/, '')))) throw new Error('metadata') const inputNames = new Set() for (const input of metadata.inputs) { if (typeof input.path !== 'string' || input.path.length < 1 || input.path.length > 512 || input.path.includes('\\') || input.path.split('/').some(part => !part || part === '.' || part === '..') || /[\x00-\x1f:]/.test(input.path) || !sha256.test(input.sha256 ?? '') || inputNames.has(input.path)) throw new Error('input') inputNames.add(input.path) } if (crypto.createHash('sha256').update(fs.readFileSync(tool)).digest('hex') !== metadata.sha256) throw new Error('tool_digest') const scratch = fs.mkdtempSync(path.join(process.env.RUNNER_TEMP, 'dc-runtime-publication-')) const home = path.join(scratch, 'home'), hooks = path.join(scratch, 'hooks'), template = path.join(scratch, 'template') for (const dir of [home, hooks, template]) fs.mkdirSync(dir) const runtimeDir = path.join(scratch, 'runtime.git') const cleanEnv = Object.fromEntries(['PATH', 'Path', 'SystemRoot', 'windir', 'TEMP', 'TMP', 'TMPDIR'] .flatMap(key => process.env[key] === undefined ? [] : [[key, process.env[key]]])) const gitEnv = { ...cleanEnv, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: home, GIT_CONFIG_GLOBAL: os.devNull, GIT_CONFIG_SYSTEM: os.devNull, GIT_CONFIG_NOSYSTEM: '1', GIT_TEMPLATE_DIR: template, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', GIT_ALLOW_PROTOCOL: 'https', GIT_OPTIONAL_LOCKS: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1' } const runGit = args => cp.execFileSync('git', ['-c', `core.hooksPath=${hooks}`, '-c', 'core.fsmonitor=false', '-c', 'gc.auto=0', '-c', 'maintenance.auto=false', ...args], { cwd: scratch, env: gitEnv, encoding: 'utf8', stdio: 'pipe', timeout: 60000, maxBuffer: 8 * 1024 * 1024 }).trim() const url = 'https://github.com/desirecore/config-center.git' const line = runGit(['ls-remote', '--refs', url, 'refs/heads/runtime']) const expectedRuntimeCommit = line ? line.split('\t')[0] : null if (line && (!sha1.test(expectedRuntimeCommit) || line !== `${expectedRuntimeCommit}\trefs/heads/runtime`)) throw new Error('runtime_ref') runGit(['init', '--bare', '--object-format=sha1', '--initial-branch=runtime', runtimeDir]) if (expectedRuntimeCommit) { // 不抓作者 main、不抓 tags、不给 depth;publisher 会核整条纯 runtime 历史。 runGit([`--git-dir=${runtimeDir}`, 'fetch', '--no-tags', '--no-recurse-submodules', '--no-write-fetch-head', url, 'refs/heads/runtime:refs/heads/runtime']) if (runGit([`--git-dir=${runtimeDir}`, 'rev-parse', 'refs/heads/runtime']) !== expectedRuntimeCommit) throw new Error('runtime_moved') } const source = path.join(process.env.GITHUB_WORKSPACE, 'runtime-source') // 使用已验证公开来源提交的作者身份,不读取 runner 或用户的全局身份配置。 const author = runGit(['-C', source, 'show', '-s', '--format=%an%x00%ae', process.env.SOURCE_SHA]).split('\0') if (author.length !== 2) throw new Error('author') const stdout = cp.execFileSync(process.execPath, [tool, '--source', source, '--source-commit', process.env.SOURCE_SHA, '--output', runtimeDir, '--apply', '--author-name', author[0], '--author-email', author[1]], { env: cleanEnv, encoding: 'utf8', stdio: 'pipe', timeout: 300000, maxBuffer: 2 * 1024 * 1024, }) const result = JSON.parse(stdout.split('\n')[0]) if (result.sourceCommit !== process.env.SOURCE_SHA || result.dryRun !== false || typeof result.changed !== 'boolean' || !sha1.test(result.runtimeCommit ?? '') || !sha256.test(result.payloadDigest ?? '') || !Number.isSafeInteger(result.dataVersion) || result.dataVersion < 1 || !Number.isSafeInteger(result.fileCount) || result.fileCount < 1 || result.fileCount > 4096 || result.validationBoundary !== 'runtime-payload-and-client-contract-only') throw new Error('result') fs.writeFileSync(path.join(process.env.RUNNER_TEMP, 'config-center-runtime-publication.json'), JSON.stringify({ sourceCommit: result.sourceCommit, runtimeDir, home, hooks, template, expectedRuntimeCommit, runtimeCommit: result.runtimeCommit, changed: result.changed, payloadDigest: result.payloadDigest, dataVersion: result.dataVersion, fileCount: result.fileCount, }), { flag: 'wx', mode: 0o600 }) console.log(result.changed ? '纯运行字节已改变;本地候选已派生,尚未推送。' : '纯运行字节未改变;没有新增 runtime 提交。') } try { main() } catch { console.error('config_center_runtime_ci_derivation_failed'); process.exitCode = 1 } NODE - name: 仅快进发布 runtime ref env: GH_TOKEN: ${{ github.token }} SOURCE_SHA: ${{ needs.verify.outputs.source_sha }} shell: bash run: | node <<'NODE' const fs = require('node:fs'), path = require('node:path'), os = require('node:os'), cp = require('node:child_process') function main() { const receipt = JSON.parse(fs.readFileSync(path.join(process.env.RUNNER_TEMP, 'config-center-runtime-publication.json'), 'utf8')) const sha1 = /^[a-f0-9]{40}$/ if (receipt.sourceCommit !== process.env.SOURCE_SHA || !sha1.test(receipt.sourceCommit) || !sha1.test(receipt.runtimeCommit) || typeof receipt.changed !== 'boolean' || (receipt.expectedRuntimeCommit !== null && !sha1.test(receipt.expectedRuntimeCommit))) throw new Error('receipt') if (!receipt.changed) { console.log('无运行内容变化;跳过推送。'); return } const cleanEnv = Object.fromEntries(['PATH', 'Path', 'SystemRoot', 'windir', 'TEMP', 'TMP', 'TMPDIR'] .flatMap(key => process.env[key] === undefined ? [] : [[key, process.env[key]]])) const gitEnv = { ...cleanEnv, HOME: receipt.home, USERPROFILE: receipt.home, XDG_CONFIG_HOME: receipt.home, GIT_CONFIG_GLOBAL: os.devNull, GIT_CONFIG_SYSTEM: os.devNull, GIT_CONFIG_NOSYSTEM: '1', GIT_TEMPLATE_DIR: receipt.template, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', GIT_ALLOW_PROTOCOL: 'https', GIT_OPTIONAL_LOCKS: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1' } const runGit = (args, auth = false) => cp.execFileSync('git', ['-c', `core.hooksPath=${receipt.hooks}`, '-c', 'core.fsmonitor=false', '-c', 'gc.auto=0', '-c', 'maintenance.auto=false', ...args], { cwd: process.env.RUNNER_TEMP, encoding: 'utf8', stdio: 'pipe', timeout: 60000, maxBuffer: 1024 * 1024, env: auth ? { ...gitEnv, GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'http.https://github.com/.extraheader', GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${process.env.GH_TOKEN}`).toString('base64')}` } : gitEnv, }).trim() const url = 'https://github.com/desirecore/config-center.git' const ref = 'refs/heads/runtime', gitDir = `--git-dir=${receipt.runtimeDir}` const remoteOid = name => { const line = runGit(['ls-remote', '--refs', url, name]) if (!line) return null const oid = line.split('\t')[0] if (!sha1.test(oid) || line !== `${oid}\t${name}`) throw new Error('ref') return oid } if (remoteOid('refs/heads/main') !== receipt.sourceCommit) { console.log('main 已前进;候选保留于本轮临时目录,不发布旧来源。'); return } if (remoteOid(ref) !== receipt.expectedRuntimeCommit) throw new Error('runtime_changed') const expectedParents = receipt.expectedRuntimeCommit ? `${receipt.runtimeCommit} ${receipt.expectedRuntimeCommit}` : receipt.runtimeCommit if (runGit([gitDir, 'rev-parse', ref]) !== receipt.runtimeCommit || runGit([gitDir, 'rev-list', '--parents', '-1', receipt.runtimeCommit]) !== expectedParents) throw new Error('parent') if (!process.env.GH_TOKEN) throw new Error('token') // 只发送一个固定 ref,不用 +/force/lease/mirror;远端须另行禁止回退及删除。 // 普通快进 push 不冒称跨管理员外部回退的绝对 expected-old lease。 runGit([gitDir, 'push', '--porcelain', '--no-follow-tags', url, `${receipt.runtimeCommit}:${ref}`], true) if (remoteOid(ref) !== receipt.runtimeCommit) throw new Error('published_ref_changed') console.log(`已发布纯运行数据 ${receipt.dataVersion},runtime ${receipt.runtimeCommit};本流程未写作者 main。`) } try { main() } catch { console.error('config_center_runtime_ci_push_failed'); process.exitCode = 1 } NODE