mirror of
https://git.openapi.site/https://github.com/desirecore/config-center.git
synced 2026-10-21 05:24:25 +08:00
254 lines
17 KiB
YAML
254 lines
17 KiB
YAML
# 上游部署模板:经审阅复制到 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
|