docs(release): record why tagged releases publish nothing
Some checks failed
deploy-site / deploy (push) Failing after 31s
Some checks failed
deploy-site / deploy (push) Failing after 31s
Two tagged release pipelines failed and no installer has been published since 1.1.0. The cause is not in the build: the release repository has no Actions secrets at all, so every run stops at the signing guard. A helper now reports which release secrets are missing and registers them once the signing material exists, and the release guide documents the required values, the Forgejo-side check, and how to re-run a pipeline for an existing tag without recreating it.
This commit is contained in:
parent
7e1972a315
commit
a85ab799a3
4 changed files with 178 additions and 14 deletions
131
scripts/ci/set-forgejo-secrets.mjs
Normal file
131
scripts/ci/set-forgejo-secrets.mjs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// scripts/ci/set-forgejo-secrets.mjs
|
||||
// Forgejo Actions 저장소 시크릿을 점검하거나 등록한다.
|
||||
//
|
||||
// 배경: 데스크톱 릴리스 워크플로(.forgejo/workflows/release.yml)는 아래 시크릿이
|
||||
// 없으면 fail-closed로 중단한다. 저장소에 시크릿이 하나도 없으면 태그를 올려도
|
||||
// 설치본이 게시되지 않는다(실측: run 49/51 모두 서명 가드에서 실패).
|
||||
//
|
||||
// 사용:
|
||||
// node scripts/ci/set-forgejo-secrets.mjs --check # 현재 상태만 확인
|
||||
// node --env-file-if-exists=.env scripts/ci/set-forgejo-secrets.mjs --write
|
||||
//
|
||||
// 값은 출력하지 않는다(이름/존재 여부/길이만). FORGEJO_TOKEN(쓰기 스코프 필요)은
|
||||
// .env 또는 환경변수에서 읽는다.
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const REQUIRED = [
|
||||
'WIN_CSC_LINK',
|
||||
'WIN_CSC_KEY_PASSWORD',
|
||||
'WIN_CSC_EXPECTED_SIGNER_SUBJECT',
|
||||
'FORGEJO_TOKEN',
|
||||
]
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const args = process.argv.slice(2)
|
||||
const write = args.includes('--write')
|
||||
|
||||
/** .env(있으면) + 환경변수에서 값 조회. 값은 로그에 절대 남기지 않는다. */
|
||||
function readEnv() {
|
||||
const values = { ...process.env }
|
||||
const envPath = join(root, '.env')
|
||||
if (existsSync(envPath)) {
|
||||
for (const line of readFileSync(envPath, 'utf8').split(/\r?\n/)) {
|
||||
if (!/^[A-Z0-9_]+=/.test(line)) continue
|
||||
const index = line.indexOf('=')
|
||||
const key = line.slice(0, index)
|
||||
if (values[key]) continue
|
||||
values[key] = line.slice(index + 1).trim()
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
const env = readEnv()
|
||||
const token = env.FORGEJO_TOKEN?.trim()
|
||||
const server = (env.GIT_SERVER_URL?.trim() || 'https://git.chanpaca.net').replace(/\/$/, '')
|
||||
const owner = env.GIT_USERNAME?.trim() || 'yunchan'
|
||||
const repo = env.GIT_REPO_NAME?.trim() || 'd3ro-voice'
|
||||
|
||||
if (!token) {
|
||||
console.error('FORGEJO_TOKEN이 필요합니다 (.env 또는 환경변수).')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const apiBase = `${server}/api/v1/repos/${owner}/${repo}/actions/secrets`
|
||||
const headers = { Authorization: `token ${token}` }
|
||||
|
||||
async function listSecrets() {
|
||||
const response = await fetch(apiBase, { headers })
|
||||
if (!response.ok) {
|
||||
throw new Error(`시크릿 목록 조회 실패: HTTP ${response.status}`)
|
||||
}
|
||||
const body = await response.json()
|
||||
return new Set((Array.isArray(body) ? body : []).map((item) => item.name))
|
||||
}
|
||||
|
||||
async function putSecret(name, value) {
|
||||
const response = await fetch(`${apiBase}/${name}`, {
|
||||
method: 'PUT',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data: value }),
|
||||
})
|
||||
if (!response.ok && response.status !== 201 && response.status !== 204) {
|
||||
throw new Error(`${name} 등록 실패: HTTP ${response.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await listSecrets()
|
||||
console.log(`저장소: ${owner}/${repo} (${server})`)
|
||||
console.log(`시크릿 API: ${apiBase}\n`)
|
||||
|
||||
let missing = 0
|
||||
for (const name of REQUIRED) {
|
||||
const present = existing.has(name)
|
||||
if (present) {
|
||||
console.log(` [x] ${name} — 등록됨`)
|
||||
continue
|
||||
}
|
||||
missing += 1
|
||||
const value = env[name]?.trim()
|
||||
console.log(` [ ] ${name} — 없음${value ? ` (환경/.env에 값 있음, 길이 ${value.length})` : ' (값 없음)'}`)
|
||||
}
|
||||
|
||||
if (missing === 0) {
|
||||
console.log('\n모든 릴리스 시크릿이 준비되었습니다.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (!write) {
|
||||
console.log(
|
||||
[
|
||||
`\n누락 ${missing}건. 값이 준비되면 다음으로 등록한다:`,
|
||||
' node --env-file-if-exists=.env scripts/ci/set-forgejo-secrets.mjs --write',
|
||||
'',
|
||||
'WIN_CSC_LINK는 public-trust Authenticode PFX를 base64로 인코딩한 값이어야 하며,',
|
||||
'WIN_CSC_EXPECTED_SIGNER_SUBJECT는 그 인증서의 정확한 subject 문자열이어야 한다.',
|
||||
'(개발용 Everything2EverythingDev 인증서는 production으로 인정되지 않는다.)',
|
||||
].join('\n'),
|
||||
)
|
||||
process.exit(missing === 0 ? 0 : 2)
|
||||
}
|
||||
|
||||
let written = 0
|
||||
for (const name of REQUIRED) {
|
||||
if (existing.has(name)) continue
|
||||
const value = env[name]?.trim()
|
||||
if (!value) {
|
||||
console.log(` 건너뜀: ${name} (값 없음)`)
|
||||
continue
|
||||
}
|
||||
await putSecret(name, value)
|
||||
written += 1
|
||||
console.log(` 등록: ${name}`)
|
||||
}
|
||||
|
||||
const after = await listSecrets()
|
||||
const stillMissing = REQUIRED.filter((name) => !after.has(name))
|
||||
console.log(`\n등록 ${written}건. 남은 누락: ${stillMissing.length ? stillMissing.join(', ') : '없음'}`)
|
||||
process.exit(stillMissing.length === 0 ? 0 : 2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue