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
|
|
@ -1,41 +1,41 @@
|
||||||
# D3RO Voice 릴리스 가이드
|
# D3RO Voice 릴리스 가이드
|
||||||
|
|
||||||
기준일: 2026-09-16. 이 문서는 desktop GitLab 패키지·자동 업데이트와 mobile store release의 경계를 분리한다. 태그 생성이나 HTTP 200 하나만으로 배포 완료를 선언하지 않는다.
|
기준일: 2026-09-18. 이 문서는 desktop GitLab 패키지·자동 업데이트와 mobile store release의 경계를 분리한다. 태그 생성이나 HTTP 200 하나만으로 배포 완료를 선언하지 않는다.
|
||||||
|
|
||||||
## 현재 release identity
|
## 현재 release identity
|
||||||
|
|
||||||
| 항목 | 정본 | 현재 판정 |
|
| 항목 | 정본 | 현재 판정 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 제품 버전 | `release/product-version.json`: `1.2.0` | source SSOT 확정 |
|
| 제품 버전 | `release/product-version.json`: `1.3.0` | source SSOT 확정 |
|
||||||
| Android | versionCode `1020001` | production AAB 미생성 |
|
| Android | versionCode `1030001` | production AAB 미생성 |
|
||||||
| iOS | build `1020001` | production archive 미검증 |
|
| iOS | build `1030001` | production archive 미검증 |
|
||||||
| Android upload key | alias `d3ro-upload-20260821`, cert SHA-256 `4F:AC:69:24:...:15:2B:54` | external PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; CI secret·복구 백업·AAB signer 대조 대기 |
|
| Android upload key | alias `d3ro-upload-20260821`, cert SHA-256 `4F:AC:69:24:...:15:2B:54` | external PKCS12·user-only ACL·Credential Manager·private-key readback GREEN; CI secret·복구 백업·AAB signer 대조 대기 |
|
||||||
| release evidence | Ed25519 public `release/mobile-release-evidence-public.pem`, keyId `2797d3e6...4a890b7f` | external private key ACL·roundtrip GREEN; CI private-key secret·복구 백업 대기 |
|
| release evidence | Ed25519 public `release/mobile-release-evidence-public.pem`, keyId `2797d3e6...4a890b7f` | external private key ACL·roundtrip GREEN; CI private-key secret·복구 백업 대기 |
|
||||||
| desktop offline license | Ed25519 public `apps/desktop/resources/license/production-public.pem`, keyId `5c52b765...81a887f` | 새 전용 keypair·external private ACL·roundtrip·desktop production build GREEN; admin `ADMIN_LICENSE_PRIVATE_KEY` secret 주입 대기 |
|
| desktop offline license | Ed25519 public `apps/desktop/resources/license/production-public.pem`, keyId `5c52b765...81a887f` | 새 전용 keypair·external private ACL·roundtrip·desktop production build GREEN; admin `ADMIN_LICENSE_PRIVATE_KEY` secret 주입 대기 |
|
||||||
| Windows Authenticode | external public-trust code-signing certificate | 현재 local installer·unpacked app은 `NotSigned`; production PFX·CI secret·signed artifact GREEN 전까지 게시 금지 |
|
| Windows Authenticode | external public-trust code-signing certificate | production PFX 없음. Forgejo 저장소 시크릿 4종(`WIN_CSC_LINK`/`WIN_CSC_KEY_PASSWORD`/`WIN_CSC_EXPECTED_SIGNER_SUBJECT`/`FORGEJO_TOKEN`)이 0건이라 릴리스 파이프라인이 fail-closed. `scripts/ci/set-forgejo-secrets.mjs --check`로 확인한다. GREEN 전까지 게시 금지 |
|
||||||
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요 |
|
| Firebase | Console `u/0`, `u/1` 모두 D3RO project 없음 | 사용자 승인 후 project·Android app 생성 필요 |
|
||||||
| AdMob | app `ca-app-pub-1039714767792854~6427959892`; banner `/9840591290`; rewarded `/2255790918` | SSOT 확정. `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료 |
|
| AdMob | app `ca-app-pub-1039714767792854~6427959892`; banner `/9840591290`; rewarded `/2255790918` | SSOT 확정. `검토 필요`·`광고 게재 제한`·store 미연결·결제 프로필 미완료 |
|
||||||
| updater feed (canonical) | `https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest` | Forgejo Generic Registry. GitLab project 1172은 legacy mirror |
|
| updater feed (canonical) | `https://git.chanpaca.net/api/packages/yunchan/generic/d3ro-voice/latest` | Forgejo Generic Registry. GitLab project 1172은 legacy mirror |
|
||||||
| release notes | `CHANGELOG.md` `## [1.2.0]` | 태그 전 확정·검증 필수 |
|
| release notes | `CHANGELOG.md` `## [1.3.0]` + Play changelog `1030001.txt` (ko/en) | 태그 전 확정·검증 필수 |
|
||||||
| 직전 게시본 | Forgejo Release `v1.1.0` (2026-09-15 게시, unsigned installer 포함) | 불변 태그. `1.2.0`은 이를 대체하는 forward-fix |
|
| 직전 게시본 | Forgejo Release `v1.1.0` (2026-09-15 게시, unsigned installer) | `v1.2.0`·`v1.3.0` 파이프라인은 Forgejo 저장소 시크릿이 없어 실패 = 게시본 없음. 설치본이 있는 마지막 버전은 `1.1.0` |
|
||||||
|
|
||||||
live canonical feed(`git.chanpaca.net/.../d3ro-voice/latest`)의 `latest.yml`은 현재 `1.1.0`을 보고한다. `1.2.0` 태그 파이프라인이 GREEN이 되면 그 값이 올라간다.
|
live canonical feed(`git.chanpaca.net/.../d3ro-voice/latest`)의 `latest.yml`은 현재 `1.1.0`을 보고한다. 시크릿을 채우고 `v1.3.0` 파이프라인이 GREEN이 되면 그 값이 `1.3.0`으로 올라가고, 그때부터 기존 설치본이 자동 업데이트를 받는다.
|
||||||
|
|
||||||
## desktop 릴리스 파이프라인
|
## desktop 릴리스 파이프라인
|
||||||
|
|
||||||
```text
|
```text
|
||||||
authoritative release commit
|
authoritative release commit
|
||||||
→ version/check/test/build GREEN
|
→ version/check/test/build GREEN
|
||||||
→ annotated tag v1.2.0
|
→ annotated tag v1.3.0
|
||||||
→ package-windows (build-win-x64)
|
→ package-windows (build-win-x64)
|
||||||
→ package-macos (build-mac-arm64)
|
→ package-macos (build-mac-arm64)
|
||||||
→ publish-release (build-linux-x64)
|
→ publish-release (build-linux-x64)
|
||||||
├─ publish-forgejo-release.mjs ← canonical
|
├─ publish-forgejo-release.mjs ← canonical
|
||||||
│ ├─ Forgejo Generic Registry /d3ro-voice/1.2.0/ (버전별 보존)
|
│ ├─ Forgejo Generic Registry /d3ro-voice/1.3.0/ (버전별 보존)
|
||||||
│ ├─ Forgejo Generic Registry /d3ro-voice/latest/ (updater feed + update-policy.json)
|
│ ├─ Forgejo Generic Registry /d3ro-voice/latest/ (updater feed + update-policy.json)
|
||||||
│ └─ Forgejo Release + CHANGELOG notes + 자산 첨부
|
│ └─ Forgejo Release + CHANGELOG notes + 자산 첨부
|
||||||
└─ publish-gitlab-release.mjs ← legacy mirror (pre-Forgejo 설치본)
|
└─ publish-gitlab-release.mjs ← legacy mirror (pre-Forgejo 설치본)
|
||||||
├─ GitLab Generic Registry /d3ro-voice/1.2.0/
|
├─ GitLab Generic Registry /d3ro-voice/1.3.0/
|
||||||
├─ GitLab Generic Registry /d3ro-voice/latest/
|
├─ GitLab Generic Registry /d3ro-voice/latest/
|
||||||
└─ GitLab Release
|
└─ GitLab Release
|
||||||
```
|
```
|
||||||
|
|
@ -50,7 +50,7 @@ authoritative release commit
|
||||||
포함되지 않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을
|
포함되지 않는다. 사이트·웹 다운로드 센터는 로컬 경로가 아니라 feed URL을
|
||||||
링크한다. (역사적 `1.0.0` 자산만 추적 상태로 남아 있다.)
|
링크한다. (역사적 `1.0.0` 자산만 추적 상태로 남아 있다.)
|
||||||
|
|
||||||
- `scripts/ci/sync-version.mjs --check --tag v1.2.0`는 태그, `release/product-version.json`, package/lockfile, Android/iOS 버전 면의 일치를 fail-closed로 검증한다.
|
- `scripts/ci/sync-version.mjs --check --tag v1.3.0`는 태그, `release/product-version.json`, package/lockfile, Android/iOS 버전 면의 일치를 fail-closed로 검증한다.
|
||||||
- `scripts/ci/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다.
|
- `scripts/ci/verify-release-metadata.mjs`는 배포 메타데이터와 CI/publisher 계약을 검증한다.
|
||||||
- 같은 gate는 desktop license public key가 Ed25519이고 `release/product-version.json`의 `desktopLicensePublicKeyId`와 일치하는지 검증한다. `electron.vite.config.ts`는 이 파일을 직접 읽으므로 누락·손상된 키로는 build가 시작되지 않는다.
|
- 같은 gate는 desktop license public key가 Ed25519이고 `release/product-version.json`의 `desktopLicensePublicKeyId`와 일치하는지 검증한다. `electron.vite.config.ts`는 이 파일을 직접 읽으므로 누락·손상된 키로는 build가 시작되지 않는다.
|
||||||
- `scripts/ci/publish-forgejo-release.mjs`는 canonical이다. 버전별 패키지를 먼저 올리고, `latest`에서 설치 자산 참조를 검증한 뒤 `latest.yml`과 `update-policy.json`을 마지막에 게시하고 공개 URL에서 재검증한다. `scripts/ci/publish-gitlab-release.mjs`는 legacy mirror로 동일 자산을 GitLab에도 올린다.
|
- `scripts/ci/publish-forgejo-release.mjs`는 canonical이다. 버전별 패키지를 먼저 올리고, `latest`에서 설치 자산 참조를 검증한 뒤 `latest.yml`과 `update-policy.json`을 마지막에 게시하고 공개 URL에서 재검증한다. `scripts/ci/publish-gitlab-release.mjs`는 legacy mirror로 동일 자산을 GitLab에도 올린다.
|
||||||
|
|
@ -110,7 +110,38 @@ https://gitlab.twentyoz.kr:8443/api/v4/projects/1172/packages/generic/d3ro-voice
|
||||||
- **회수(rollback)**: `stagingPercentage`를 낮추거나 `killSwitch`를 켠다. 이미 배포된 버전은 되돌리지 않고 더 높은 patch로 forward-fix한다.
|
- **회수(rollback)**: `stagingPercentage`를 낮추거나 `killSwitch`를 켠다. 이미 배포된 버전은 되돌리지 않고 더 높은 patch로 forward-fix한다.
|
||||||
|
|
||||||
|
|
||||||
## `1.2.0` 릴리스 절차
|
## `1.3.0` 릴리스 절차 (canonical = Forgejo Actions)
|
||||||
|
|
||||||
|
**선행 조건 — Forgejo 저장소 시크릿.** `.forgejo/workflows/release.yml`은 아래 4개가
|
||||||
|
없으면 fail-closed로 중단한다. 실측(2026-09-18): 저장소 시크릿이 0건이라 `v1.2.0`과
|
||||||
|
`v1.3.0` 태그 파이프라인이 서명 가드에서 실패했고, 그래서 설치본·업데이트가 게시되지
|
||||||
|
않았다. 현재 상태 점검과 등록은 다음 한 줄로 한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/ci/set-forgejo-secrets.mjs --check
|
||||||
|
node --env-file-if-exists=.env scripts/ci/set-forgejo-secrets.mjs --write
|
||||||
|
```
|
||||||
|
|
||||||
|
| 시크릿 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| `WIN_CSC_LINK` | public-trust Authenticode PFX (base64). 개발용 `Everything2EverythingDev`는 거부된다 |
|
||||||
|
| `WIN_CSC_KEY_PASSWORD` | PFX password |
|
||||||
|
| `WIN_CSC_EXPECTED_SIGNER_SUBJECT` | 인증서의 정확한 subject. `verify-windows-release-artifact.ps1`이 이 identity를 요구한다 |
|
||||||
|
| `FORGEJO_TOKEN` | `write:package` + `write:repository` (릴리스 게시 단계) |
|
||||||
|
|
||||||
|
1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다.
|
||||||
|
2. `CHANGELOG.md` `## [1.3.0] - 2026-09-18` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션과 그에 대응하는 Play changelog(`apps/mobile-rn/metadata/android/*/changelogs/<versionCode>.txt`)가 없으면 실패한다.
|
||||||
|
3. dirty/untracked 작업을 임의로 reset·clean하지 말고, release 범위만 검토 가능한 authoritative commit으로 보존한다.
|
||||||
|
4. 같은 commit에서 lint, typecheck, test, build, release metadata·security·artifact gate를 전부 GREEN으로 만든다.
|
||||||
|
5. **로컬 전사 엔진 검증**: 파이프라인은 `sidecar:setup` → `sidecar:build` → `verify-sidecar-bundle.mjs`를 패키징 전에 실행한다. 이 게이트가 없으면 설치본에 엔진이 빠진 채 게시된다(과거 실제 사고). 로컬에서 `electron-builder --dir`만 볼 때는 서명 실패로 extraResources 복사가 중간에 끊기므로 `-c.win.forceCodeSigning=false`로 확인한다.
|
||||||
|
6. desktop offline license를 제공한다면 external private key를 admin의 `ADMIN_LICENSE_PRIVATE_KEY` secret로 주입하고, 저장소 public key와 sign/verify roundtrip 및 발급 감사 로그를 확인한다.
|
||||||
|
7. 이미 게시된 버전보다 높은 annotated 태그 `v1.3.0`을 생성해 push한다. `npm run release:tag -- --dry-run`으로 검증한 뒤 `npm run release:tag`(GPG 사용 시 `-- --sign`)와 `git push chanpaca v1.3.0`를 실행한다. 태그는 불변이며 게이트를 시작하는 후속 단계지 검증을 대체하지 않는다. 이미 게시된 버전을 재게시하지 않는다: canonical publisher는 버전별 자산이 다른 바이트를 가지면 fail-closed로 중단한다.
|
||||||
|
8. 시크릿을 나중에 채웠다면 태그를 새로 만들 필요가 없다 — `release.yml`은 `workflow_dispatch`를 지원하므로 Forgejo UI에서 해당 태그 ref로 수동 실행한다.
|
||||||
|
9. Forgejo Actions run(`/actions/tasks` API 또는 UI)에서 단계별 결과를 확인한다. pending/stuck/skipped를 GREEN으로 기록하지 않는다.
|
||||||
|
10. Forgejo Release note/asset, `latest.yml`, `update-policy.json`, installer hash를 외부 public URL에서 다시 검증한다.
|
||||||
|
11. 이전 설치본에서 자동 업데이트 E2E를 실행하고 실행 중 버전·프로세스·사용자 데이터 보존, 그리고 **업데이트 후 로컬 받아쓰기 1회 성공**을 확인한다.
|
||||||
|
|
||||||
|
## `1.2.0` 릴리스 절차 (기록)
|
||||||
|
|
||||||
1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다.
|
1. `release/product-version.json`의 version/build 값과 모든 버전 면을 `npm run version:check`로 대조한다.
|
||||||
2. `CHANGELOG.md` `## [1.2.0] - 2026-09-16` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션이 없으면 실패해야 한다.
|
2. `CHANGELOG.md` `## [1.2.0] - 2026-09-16` 섹션을 사용자 변경점 중심으로 확정한다. publisher는 이 섹션이 없으면 실패해야 한다.
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| GAP-QA-01 | Quality | Extreme Red Team: headful end-to-end bug hunting across real desktop Electron, Web Next.js, and CI pipelines. | `red_team_log.md`, `tests/e2e/red_team_cycle*.spec.ts`, `apps/web/e2e/red_team_cycle4_web.spec.ts` | `[x]` 2026-09-15: 18 scenarios executed, 14 defects caught and 100% resolved (infinite chunking loop DEF-008, IPC signature mismatch DEF-004, markdown editor typing rollback DEF-006, Web RSC Link serialization DEF-012, secret scanner lookahead DEF-013, etc.). All 18 scenarios GREEN with zero regressions. |
|
| GAP-QA-01 | Quality | Extreme Red Team: headful end-to-end bug hunting across real desktop Electron, Web Next.js, and CI pipelines. | `red_team_log.md`, `tests/e2e/red_team_cycle*.spec.ts`, `apps/web/e2e/red_team_cycle4_web.spec.ts` | `[x]` 2026-09-15: 18 scenarios executed, 14 defects caught and 100% resolved (infinite chunking loop DEF-008, IPC signature mismatch DEF-004, markdown editor typing rollback DEF-006, Web RSC Link serialization DEF-012, secret scanner lookahead DEF-013, etc.). All 18 scenarios GREEN with zero regressions. |
|
||||||
| GAP-REL-01 | Release | Official release publication to Forgejo and active public download center deployment. | `scripts/ci/publish-forgejo-release.mjs`, `apps/web/src/app/download/page.tsx`, `site/src/sections/Download.tsx`, `apps/web/e2e/red_team_cycle4_web.spec.ts` | `[~]` 2026-09-15: v1.1.0 release assets (`D3RO-Voice-Setup-1.1.0-x64.exe`, `.blockmap`, `latest.yml`, `update-policy.json`) published to canonical Forgejo registry and release hub. 2026-09-16: the published 1.1.0 installer carries no Authenticode signature, so it does not satisfy the release policy; product version moved to `1.2.0` and publication must come from CI with the signing gate GREEN. Download centers in `apps/web` (`/download`) and `site` (`#download`) link the canonical Forgejo feed. |
|
| GAP-REL-01 | Release | Official release publication to Forgejo and active public download center deployment. | `scripts/ci/publish-forgejo-release.mjs`, `apps/web/src/app/download/page.tsx`, `site/src/sections/Download.tsx`, `apps/web/e2e/red_team_cycle4_web.spec.ts` | `[~]` 2026-09-15: v1.1.0 release assets (`D3RO-Voice-Setup-1.1.0-x64.exe`, `.blockmap`, `latest.yml`, `update-policy.json`) published to canonical Forgejo registry and release hub. 2026-09-16: the published 1.1.0 installer carries no Authenticode signature, so it does not satisfy the release policy; product version moved to `1.2.0` and publication must come from CI with the signing gate GREEN. Download centers in `apps/web` (`/download`) and `site` (`#download`) link the canonical Forgejo feed. |
|
||||||
| GAP-REL-02 | Release | Windows stable publication needs an external public-trust Authenticode PFX, its password, the exact signer subject, and a Forgejo token, none of which live in the repository. | `.forgejo/workflows/release.yml`, `.gitlab-ci.yml`, `scripts/ci/verify-windows-release-artifact.ps1` | `[!]` 2026-09-16: every publisher fails closed without `WIN_CSC_*` and `FORGEJO_TOKEN`; provide them as protected CI secrets, then re-run the tag pipeline. Still open as of `1.3.0` (2026-09-18): the `v1.3.0` tag must be built by CI with the signing gate GREEN. Local packaging cannot produce a signed installer (`forceCodeSigning: true`). |
|
| GAP-REL-02 | Release | Windows stable publication needs an external public-trust Authenticode PFX, its password, the exact signer subject, and a Forgejo token, none of which live in the repository. | `.forgejo/workflows/release.yml`, `.gitlab-ci.yml`, `scripts/ci/set-forgejo-secrets.mjs`, `scripts/ci/verify-windows-release-artifact.ps1` | `[!]` 2026-09-18 measured: the Forgejo repo has **zero** Actions secrets (`GET /repos/yunchan/d3ro-voice/actions/secrets` → `[]`), so `v1.2.0` (run 49) and `v1.3.0` (run 51) both failed at the signing guard and **no installer or update has been published since `1.1.0`**. Inject the four secrets (`WIN_CSC_LINK`, `WIN_CSC_KEY_PASSWORD`, `WIN_CSC_EXPECTED_SIGNER_SUBJECT`, `FORGEJO_TOKEN`) with `npm run release:secrets` (check: `npm run release:secrets:check`), then re-run `release.yml` for the `v1.3.0` tag via `workflow_dispatch` (tags are immutable). |
|
||||||
| GAP-ADS-01 | Ads | 9 of 10 desktop ad adapters still extend `UnavailableAdAdapter` (`provider_not_integrated`). | `apps/desktop/src/main/services/ads/*` | `[~]` 2026-09-13: `DirectHouseSponsorAdapter` is now a real configurable REST adapter (bid/impression/click/reward via `endpointUrl`; fail-closed when unconfigured; 22 unit tests GREEN). Remaining 9 need official SDKs/authenticated endpoints. |
|
| GAP-ADS-01 | Ads | 9 of 10 desktop ad adapters still extend `UnavailableAdAdapter` (`provider_not_integrated`). | `apps/desktop/src/main/services/ads/*` | `[~]` 2026-09-13: `DirectHouseSponsorAdapter` is now a real configurable REST adapter (bid/impression/click/reward via `endpointUrl`; fail-closed when unconfigured; 22 unit tests GREEN). Remaining 9 need official SDKs/authenticated endpoints. |
|
||||||
| GAP-ADS-02 | Ads | Desktop mediation reward accounting is not wired to license quota (`claimReward` still returns no tokens). | `AdMediationEngine.ts`, `AppLayout.tsx` | Wire verified `reportRewardCompletion` to `LicenseService` quota after the direct sponsor endpoint exists. |
|
| GAP-ADS-02 | Ads | Desktop mediation reward accounting is not wired to license quota (`claimReward` still returns no tokens). | `AdMediationEngine.ts`, `AppLayout.tsx` | Wire verified `reportRewardCompletion` to `LicenseService` quota after the direct sponsor endpoint exists. |
|
||||||
| GAP-ID-01 | Identity | Supabase, .NET JWT/SQLite, and the desktop offline license each had their own tier/role shape. | `@d3ro/core/entitlement`, `LicenseService`, `entitlement-context` | `[~]` 2026-09-13: canonical `EntitlementSnapshot` + `resolveEntitlement` added with tests; desktop tier normalization + `isPro` fixed. Full adoption tracked as GAP-ID-02. |
|
| GAP-ID-01 | Identity | Supabase, .NET JWT/SQLite, and the desktop offline license each had their own tier/role shape. | `@d3ro/core/entitlement`, `LicenseService`, `entitlement-context` | `[~]` 2026-09-13: canonical `EntitlementSnapshot` + `resolveEntitlement` added with tests; desktop tier normalization + `isPro` fixed. Full adoption tracked as GAP-ID-02. |
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@
|
||||||
"release:forgejo:local": "node --env-file-if-exists=.env scripts/ci/publish-forgejo-release.mjs",
|
"release:forgejo:local": "node --env-file-if-exists=.env scripts/ci/publish-forgejo-release.mjs",
|
||||||
"release:forgejo:check": "node scripts/ci/publish-forgejo-release.mjs --check",
|
"release:forgejo:check": "node scripts/ci/publish-forgejo-release.mjs --check",
|
||||||
"release:tag": "node scripts/ci/create-release-tag.mjs",
|
"release:tag": "node scripts/ci/create-release-tag.mjs",
|
||||||
|
"release:secrets": "node --env-file-if-exists=.env scripts/ci/set-forgejo-secrets.mjs --write",
|
||||||
|
"release:secrets:check": "node scripts/ci/set-forgejo-secrets.mjs --check",
|
||||||
"security:secrets": "node scripts/ci/check-no-hardcoded-secrets.mjs",
|
"security:secrets": "node scripts/ci/check-no-hardcoded-secrets.mjs",
|
||||||
"security:secrets:test": "node scripts/ci/check-no-hardcoded-secrets.mjs --self-test",
|
"security:secrets:test": "node scripts/ci/check-no-hardcoded-secrets.mjs --self-test",
|
||||||
"check:design": "node scripts/ci/check-design-tokens.mjs",
|
"check:design": "node scripts/ci/check-design-tokens.mjs",
|
||||||
|
|
|
||||||
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