diff --git a/.agents/skills/tablecloth-promote-stable/SKILL.md b/.agents/skills/tablecloth-promote-stable/SKILL.md new file mode 100644 index 00000000..e7dd4fc5 --- /dev/null +++ b/.agents/skills/tablecloth-promote-stable/SKILL.md @@ -0,0 +1,32 @@ +--- +name: tablecloth-promote-stable +description: Promote a tested TableCloth develop line to a stable X.Y.0 Retail release on main, including freeze, synchronization, merge, tagging, signing, publishing, and transition checks. Use when ending a Preview cycle. +--- + +# TableCloth 정식 버전 승격 + +검증한 Preview 개발선을 `main`에 병합하고 `vX.Y.0` Retail로 게시합니다. + +## 승격 준비 + +[`docs/BRANCHING.md`](../../../docs/BRANCHING.md), [`docs/RELEASE_CHANNELS.md`](../../../docs/RELEASE_CHANNELS.md), [`docs/RELEASING.md`](../../../docs/RELEASING.md)를 읽습니다. 목표 버전, 마지막 Preview, 미해결 차단 이슈와 릴리스 노트 범위를 확인합니다. + +새 기능 병합을 중지하고 최신 `main` 핫픽스를 `develop`에 반영합니다. `Directory.Build.Props`가 목표 `X.Y.0.0`을 유지하는지 확인합니다. + +## 최종 검증과 병합 + +x64와 arm64 CI와 두 테스트 프로젝트를 완료합니다. TableCloth 본체와 그 밖의 호스트 시나리오는 호스트 Windows에서 스모크 테스트하고, Spork 게스트 시나리오만 Windows Sandbox 안에서 확인합니다. TableCloth와 Spork의 주요 실행 경로 및 업데이트 채널 동작을 확인합니다. + +검증을 통과하면 Pull Request로 `develop`을 `main`에 병합합니다. 정식 태그는 병합된 `origin/main` HEAD와 정확히 일치해야 합니다. Preview 태그가 가리키던 병합 전 커밋이나 로컬 전용 커밋에 태그하지 않습니다. + +## Retail 게시 + +Retail 태그 Push 뒤 [`build.yml`](../../../.github/workflows/build.yml)의 Draft와 두 아키텍처 산출물을 확인합니다. [`tablecloth-sign-release`](../tablecloth-sign-release/SKILL.md)를 Retail 모드로 수행하고 [`tablecloth-verify-release`](../tablecloth-verify-release/SKILL.md)로 게시 전후 결과를 확인합니다. + +릴리스 노트에는 Preview 기간의 주요 기능, 호환성 영향, 알려진 문제와 마이그레이션 사항을 사용자 관점에서 정리합니다. Avalonia와 Native AOT처럼 이전 버전 대비 기반 기술이 바뀌었다면 실행 성능과 이후 플랫폼 확장에 미치는 범위를 함께 설명합니다. + +## 승격 이후 상태 + +Preview 사용자가 Retail로 자동 전환된다고 가정하지 않습니다. 현재 수동 채널 전환 정책을 릴리스 노트와 지원 문서에 반영합니다. + +정식 게시와 후속 자동화를 확인한 뒤 다음 Minor 버전 개발 요청이 있으면 [`tablecloth-start-next-version`](../tablecloth-start-next-version/SKILL.md)을 수행합니다. `develop` 삭제나 재생성은 미병합 이력을 확인한 뒤 결정합니다. diff --git a/.agents/skills/tablecloth-promote-stable/agents/openai.yaml b/.agents/skills/tablecloth-promote-stable/agents/openai.yaml new file mode 100644 index 00000000..b249f48d --- /dev/null +++ b/.agents/skills/tablecloth-promote-stable/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Promote TableCloth Stable" + short_description: "Promote a Preview line to stable Retail" + default_prompt: "Use $tablecloth-promote-stable to promote this TableCloth version to Retail." diff --git a/.agents/skills/tablecloth-release-hotfix/SKILL.md b/.agents/skills/tablecloth-release-hotfix/SKILL.md new file mode 100644 index 00000000..74a21d15 --- /dev/null +++ b/.agents/skills/tablecloth-release-hotfix/SKILL.md @@ -0,0 +1,32 @@ +--- +name: tablecloth-release-hotfix +description: Deliver an urgent TableCloth Retail patch from main and forward-port the fix to develop without changing its next-minor version. Use for backward-compatible X.Y.Z fixes, not feature releases. +--- + +# TableCloth Retail 핫픽스 + +현재 정식 버전의 호환성 문제를 `X.Y.Z` Retail 패치로 게시하고 수정 코드를 다음 Minor 버전에 전파합니다. + +## 패치 범위 + +[`docs/BRANCHING.md`](../../../docs/BRANCHING.md)와 [`docs/RELEASING.md`](../../../docs/RELEASING.md)를 읽습니다. 최신 `origin/main`과 정식 Release를 확인하고 다음 Patch 번호를 선택합니다. + +긴급 수정이 새 공개 기능이나 호환되지 않는 변경을 포함하면 핫픽스로 게시하지 않습니다. 다음 Minor 또는 Major 버전 경로로 전환합니다. + +## 수정과 버전 커밋 + +최신 `main`에서 `hotfix/X.Y.Z`를 만듭니다. 실제 수정과 회귀 테스트를 먼저 커밋하고 `Directory.Build.Props`의 Patch 변경을 별도 커밋으로 남깁니다. Revision은 `0`을 유지합니다. + +관련 단위 테스트를 실행합니다. TableCloth 본체와 그 밖의 호스트 시나리오는 호스트 Windows에서 스모크 테스트하고, Spork 게스트 시나리오만 Windows Sandbox 안에서 확인합니다. Pull Request를 통해 `main`에 병합하고 새 `origin/main` HEAD에만 Retail 태그를 생성합니다. + +## Retail 게시 + +[`build.yml`](../../../.github/workflows/build.yml)이 생성한 Draft와 x64 및 arm64 `PublishPayload`를 확인합니다. [`tablecloth-sign-release`](../tablecloth-sign-release/SKILL.md)를 Retail 모드로 수행하고 [`tablecloth-verify-release`](../tablecloth-verify-release/SKILL.md)로 게시 전후 상태를 확인합니다. + +정식 게시 뒤 WinGet Pull Request와 Discord 공지의 실제 생성 결과를 확인합니다. + +## 다음 버전 순방향 전파 + +`develop`이 존재하면 패치 코드 커밋을 즉시 전파합니다. 버전 변경 커밋은 전파하지 않으며 `develop`의 `X.(Y+1).0`을 유지합니다. 같은 회귀 테스트를 `develop`에서도 실행합니다. + +브랜치 정리는 병합, Retail 게시와 순방향 전파를 모두 확인한 뒤 수행합니다. 이전 버전 유지보수 계획이 남아 있으면 관련 브랜치를 보존합니다. diff --git a/.agents/skills/tablecloth-release-hotfix/agents/openai.yaml b/.agents/skills/tablecloth-release-hotfix/agents/openai.yaml new file mode 100644 index 00000000..a76939bc --- /dev/null +++ b/.agents/skills/tablecloth-release-hotfix/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Release TableCloth Hotfix" + short_description: "Deliver and forward-port a Retail hotfix" + default_prompt: "Use $tablecloth-release-hotfix to deliver this TableCloth Retail patch." diff --git a/.agents/skills/tablecloth-release-preview/SKILL.md b/.agents/skills/tablecloth-release-preview/SKILL.md new file mode 100644 index 00000000..ca749e19 --- /dev/null +++ b/.agents/skills/tablecloth-release-preview/SKILL.md @@ -0,0 +1,36 @@ +--- +name: tablecloth-release-preview +description: Prepare and publish a TableCloth Preview from develop with a strict preview.N tag, native x64 and arm64 CI artifacts, local signing, and prerelease verification. Use for Preview releases, not Retail patches or stable promotion. +--- + +# TableCloth Preview 릴리스 + +`develop`의 다음 Minor 버전을 `vX.Y.0-preview.N` Prerelease로 게시합니다. + +## 준비 상태 + +[`docs/BRANCHING.md`](../../../docs/BRANCHING.md), [`docs/RELEASE_CHANNELS.md`](../../../docs/RELEASE_CHANNELS.md), [`docs/RELEASING.md`](../../../docs/RELEASING.md)를 읽습니다. 다음 조건을 확인합니다. + +- 대상 커밋이 `origin/develop` 이력에 포함됨 +- 최신 `main` 핫픽스가 `develop`에 반영됨 +- `Directory.Build.Props`가 목표 버전 코어와 일치함 +- 로컬 빌드와 관련 테스트가 성공함 +- 기존 Preview 태그에서 다음 번호를 계산함 + +태그는 `^v[0-9]+\.[0-9]+\.[0-9]+-preview\.[1-9][0-9]*$` 형식만 허용합니다. 게시하거나 삭제한 Preview 번호를 재사용하지 않습니다. + +## CI Draft 생성 + +사용자가 Preview 릴리스를 실행하도록 요청했다면 태그를 생성하고 Push합니다. [`preview.yml`](../../../.github/workflows/preview.yml)의 x64와 arm64 Job, Draft Prerelease 생성, 두 `PublishPayload` 아티팩트를 확인합니다. + +CI가 실패하면 태그를 이동하지 않습니다. 원인을 새 커밋에서 수정하고 다음 Preview 번호를 사용합니다. + +## 서명과 게시 + +CI Draft가 성공하면 [`tablecloth-sign-release`](../tablecloth-sign-release/SKILL.md)를 읽고 Preview 모드로 수행합니다. `--preview-number`에는 태그의 `N`을 전달합니다. + +서명 뒤 [`tablecloth-verify-release`](../tablecloth-verify-release/SKILL.md)를 읽고 Preview 자산, Preview 채널 메타데이터와 Prerelease 상태를 검증합니다. 검증을 통과하고 사용자가 게시까지 요청했다면 Draft를 Prerelease로 게시합니다. + +Preview에는 WinGet 제출, Discord 정식 공지, 무설치 고정 URL 별칭을 만들지 않습니다. Preview를 최신 정식 Release로 지정하지 않습니다. + +완료 보고에는 태그, Release URL, CI 실행, 서명 검증과 알려진 제한을 포함합니다. diff --git a/.agents/skills/tablecloth-release-preview/agents/openai.yaml b/.agents/skills/tablecloth-release-preview/agents/openai.yaml new file mode 100644 index 00000000..600b8137 --- /dev/null +++ b/.agents/skills/tablecloth-release-preview/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Release TableCloth Preview" + short_description: "Build and publish a signed Preview release" + default_prompt: "Use $tablecloth-release-preview to publish the next signed TableCloth Preview." diff --git a/.agents/skills/tablecloth-release/SKILL.md b/.agents/skills/tablecloth-release/SKILL.md new file mode 100644 index 00000000..3d83bfe6 --- /dev/null +++ b/.agents/skills/tablecloth-release/SKILL.md @@ -0,0 +1,61 @@ +--- +name: tablecloth-release +description: Orchestrate the TableCloth branch, version, Preview, hotfix, signing, stable promotion, and post-release lifecycle. Use for end-to-end release management requests; use a narrower TableCloth child skill when the request covers only one phase. +--- + +# TableCloth 릴리스 오케스트레이션 + +TableCloth의 다음 버전 개발 시작부터 정식 게시까지 단계별 상태를 관리합니다. 이 스킬은 작업을 직접 포괄하기보다 필요한 하위 스킬을 선택하고 완료 조건을 연결합니다. + +## 기준 문서 + +작업을 시작할 때 다음 문서를 읽습니다. + +- [`docs/BRANCHING.md`](../../../docs/BRANCHING.md): 브랜치 역할과 버전 증가 기준 +- [`docs/RELEASE_CHANNELS.md`](../../../docs/RELEASE_CHANNELS.md): Retail과 Preview 채널 계약 +- [`docs/RELEASING.md`](../../../docs/RELEASING.md): CI Draft, SimplySign 서명과 게시 절차 + +문서와 실제 워크플로가 다르면 `.github/workflows`, `build.cs`, `Directory.Build.Props`의 현재 동작을 확인하고 차이를 보고합니다. 확인하지 않은 문서 설명을 현재 구현으로 단정하지 않습니다. + +## 상태 확인 + +다음 항목으로 현재 릴리스 단계를 판별합니다. + +- 현재 브랜치와 작업 트리 +- `origin/main`과 `origin/develop`의 존재 및 선후 관계 +- 최신 정식 태그와 Preview 태그 +- `Directory.Build.Props`의 버전 코어 +- GitHub Release의 Draft, Prerelease와 게시 상태 +- 관련 GitHub Actions 실행 결과 + +검토나 계획만 요청받았다면 외부 상태를 변경하지 않습니다. 릴리스 실행을 요청받았다면 태그 Push, Draft 생성, 서명 자산 업로드와 게시를 요청 범위에 맞추어 이어갑니다. + +## 하위 스킬 선택 + +선택한 하위 스킬의 `SKILL.md`를 작업 전에 모두 읽습니다. + +| 요청 | 하위 스킬 | +| --- | --- | +| 다음 Minor 버전 개발 시작 | [`tablecloth-start-next-version`](../tablecloth-start-next-version/SKILL.md) | +| Preview 생성과 게시 | [`tablecloth-release-preview`](../tablecloth-release-preview/SKILL.md) | +| 현재 Retail 긴급 패치 | [`tablecloth-release-hotfix`](../tablecloth-release-hotfix/SKILL.md) | +| Preview의 정식 승격 | [`tablecloth-promote-stable`](../tablecloth-promote-stable/SKILL.md) | +| CI 산출물의 로컬 서명 | [`tablecloth-sign-release`](../tablecloth-sign-release/SKILL.md) | +| 게시 전후 검증 | [`tablecloth-verify-release`](../tablecloth-verify-release/SKILL.md) | + +Preview 릴리스는 Preview 스킬, 서명 스킬, 검증 스킬 순서로 진행합니다. 핫픽스와 정식 승격도 각 준비 스킬 뒤에 서명과 검증 스킬을 연결합니다. + +## 공통 불변 조건 + +- Retail 태그는 `origin/main` HEAD와 정확히 일치합니다. +- Preview 태그는 `origin/develop` 이력에 포함됩니다. +- Preview 태그는 `vX.Y.Z-preview.N` 형식을 사용합니다. +- `Directory.Build.Props`의 코어 버전과 태그 코어를 일치시킵니다. +- x64와 arm64 산출물을 모두 확보하고 모든 서명 검증을 통과한 뒤 게시합니다. +- Retail 핫픽스를 `develop`으로 전파하면서 다음 Minor 버전을 유지합니다. +- Preview 게시에서는 WinGet과 Discord 자동화를 실행하지 않습니다. +- Retail 게시에서는 WinGet Pull Request와 Discord 공지의 실제 결과를 각각 확인합니다. + +## 중단 조건 + +서명 실패, 아키텍처 누락, 태그와 브랜치 불일치, 버전 불일치, 부분 업로드가 발견되면 Draft를 유지하고 원인을 보고합니다. 이미 외부에 Push한 태그를 다른 커밋으로 이동하지 않습니다. 게시 권한이 요청에 포함되지 않았다면 서명과 검증 결과까지만 제공합니다. diff --git a/.agents/skills/tablecloth-release/agents/openai.yaml b/.agents/skills/tablecloth-release/agents/openai.yaml new file mode 100644 index 00000000..b4a1127b --- /dev/null +++ b/.agents/skills/tablecloth-release/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "TableCloth Release" + short_description: "Orchestrate version development and releases" + default_prompt: "Use $tablecloth-release to manage this TableCloth release lifecycle." diff --git a/.agents/skills/tablecloth-sign-release/SKILL.md b/.agents/skills/tablecloth-sign-release/SKILL.md new file mode 100644 index 00000000..2a744f1d --- /dev/null +++ b/.agents/skills/tablecloth-sign-release/SKILL.md @@ -0,0 +1,41 @@ +--- +name: tablecloth-sign-release +description: Repackage and Authenticode-sign TableCloth and Spork x64 and arm64 CI payloads with the local SimplySign certificate, then replace and verify draft release assets. Use only after a release CI draft succeeds. +--- + +# TableCloth 릴리스 산출물 서명 + +CI가 만든 x64와 arm64 게시 산출물을 로컬 SimplySign 인증서로 전체 서명하고 GitHub Draft 자산을 교체합니다. + +## 입력 검증 + +[`docs/RELEASING.md`](../../../docs/RELEASING.md)의 로컬 서명 절차를 읽습니다. 다음 입력을 확인합니다. + +- 대상 태그와 Retail 또는 Preview 구분 +- 성공한 CI 실행과 Draft Release +- `PublishPayload-x64`와 `PublishPayload-arm64` +- 태그 코어와 `Directory.Build.Props`의 일치 +- Preview라면 태그에서 추출한 Preview 번호 +- SimplySign 세션과 개인 키를 포함한 로컬 인증서 + +인증서 개인 키나 PFX를 복사하거나 저장소와 CI에 업로드하지 않습니다. 인증서 주체는 현재 로컬 인증서에서 확인하며 문서에 고정된 이름을 가정하지 않습니다. + +## 안전한 작업 디렉터리 + +`git rev-parse --show-toplevel`로 저장소 루트를 확인합니다. 정리 대상은 해당 루트 아래의 생성물인 `publish`와 `Releases`, 그리고 작업별 임시 아티팩트 디렉터리로 제한합니다. 계산한 경로가 저장소 루트 또는 임시 디렉터리 안에 있는지 확인한 뒤 제거합니다. + +두 `PublishPayload`를 내려받아 `publish` 계약에 맞게 합칩니다. TableCloth와 Spork의 x64 및 arm64 폴더가 모두 존재하지 않으면 중단합니다. + +## 패키징과 서명 + +Retail은 `build.cmd --skip-build --sign`을 사용합니다. Preview는 `--preview --preview-number N`을 추가하며 `N`을 태그에서 추출합니다. 수동 기본값에 의존하지 않습니다. + +패키징 로그에서 TableCloth와 Spork의 앱 바이너리, `Update.exe`, `Setup.exe` 서명을 확인합니다. 로컬에서 빌드할 수 없는 arm64 Native AOT 코드는 CI 페이로드를 사용하고 x64 호스트에서는 패키징과 서명만 수행합니다. + +## 업로드와 검증 + +자산을 파일별로 `gh release upload --clobber`하여 부분 실패를 식별합니다. 각 파일을 최대 세 번 재시도하고 계속 실패하면 Draft를 유지합니다. + +원격 자산 이름과 크기를 로컬 결과와 비교합니다. 모든 `.exe` 자산과 Portable ZIP 내부 앱 바이너리의 Authenticode 상태가 `Valid`인지 확인합니다. 한 항목이라도 누락되거나 서명이 유효하지 않으면 `UNSIGNED` 경고를 제거하거나 Release를 게시하지 않습니다. + +완료 보고에는 사용한 CI 실행, 두 아키텍처의 산출물 수, 서명 검증 결과와 업로드 대조 결과를 포함합니다. 인증서의 민감한 정보는 출력하지 않습니다. diff --git a/.agents/skills/tablecloth-sign-release/agents/openai.yaml b/.agents/skills/tablecloth-sign-release/agents/openai.yaml new file mode 100644 index 00000000..d85ab786 --- /dev/null +++ b/.agents/skills/tablecloth-sign-release/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Sign TableCloth Release" + short_description: "Sign x64 and arm64 release artifacts" + default_prompt: "Use $tablecloth-sign-release to sign and replace the draft assets for this release." diff --git a/.agents/skills/tablecloth-start-next-version/SKILL.md b/.agents/skills/tablecloth-start-next-version/SKILL.md new file mode 100644 index 00000000..169d9192 --- /dev/null +++ b/.agents/skills/tablecloth-start-next-version/SKILL.md @@ -0,0 +1,39 @@ +--- +name: tablecloth-start-next-version +description: Start development of the next TableCloth minor version on develop, including version selection, branch preparation, version-source updates, and baseline verification. Use when beginning a new minor cycle, not for a patch or an existing Preview release. +--- + +# TableCloth 다음 버전 개발 시작 + +최신 Retail을 기준으로 다음 Minor 버전의 `develop` 브랜치와 버전 코어를 준비합니다. + +## 기준 확인 + +[`docs/BRANCHING.md`](../../../docs/BRANCHING.md)를 읽고 다음 상태를 현재 저장소와 원격에서 확인합니다. + +- 최신 게시 Retail 태그와 `origin/main` HEAD +- 기존 `origin/develop`의 존재와 미병합 커밋 +- `Directory.Build.Props`의 현재 버전 +- 작업 트리와 서브모듈 상태 + +최신 정식 버전이 `X.Y.Z`라면 기본 다음 버전은 `X.(Y+1).0`입니다. 사용자가 다른 목표 버전을 지정하면 SemVer 증가 방향과 현재 브랜치 정책의 충돌 여부를 먼저 검토합니다. + +## 브랜치와 버전 준비 + +기존 `develop`이 없으면 최신 `origin/main`에서 만듭니다. 기존 브랜치가 있으면 덮어쓰지 않고 `main`과의 선후 관계 및 미병합 작업을 확인합니다. + +`Directory.Build.Props`에서 Major, Minor, Patch와 Revision을 목표 버전에 맞춥니다. 다음 Minor 버전은 Patch와 Revision을 `0`으로 둡니다. Preview 접미사는 이 파일에 넣지 않습니다. + +버전 변경은 기능 변경과 분리한 커밋으로 남깁니다. 원격 Push나 Pull Request 생성은 사용자가 개발 착수를 실행하도록 요청한 범위에서만 수행합니다. + +## 기준선 검증 + +서브모듈을 초기화한 뒤 저장소의 전체 빌드와 두 테스트 프로젝트를 실행합니다. 기능 변경 전 실행 기준선이 필요하면 TableCloth 본체를 호스트 Windows에서 스모크 테스트합니다. Spork 게스트 시나리오를 검증할 때만 Windows Sandbox 안에서 실행합니다. + +완료 보고에는 다음 내용을 포함합니다. + +- 목표 버전과 브랜치 +- 버전 변경 커밋 +- 빌드와 테스트 결과 +- 원격 Push 또는 Pull Request 상태 +- 첫 Preview를 만들기 전에 남은 작업 diff --git a/.agents/skills/tablecloth-start-next-version/agents/openai.yaml b/.agents/skills/tablecloth-start-next-version/agents/openai.yaml new file mode 100644 index 00000000..9e9b10f5 --- /dev/null +++ b/.agents/skills/tablecloth-start-next-version/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Start TableCloth Version" + short_description: "Start the next TableCloth minor version" + default_prompt: "Use $tablecloth-start-next-version to begin the next TableCloth minor cycle." diff --git a/.agents/skills/tablecloth-verify-release/SKILL.md b/.agents/skills/tablecloth-verify-release/SKILL.md new file mode 100644 index 00000000..4cc7e17b --- /dev/null +++ b/.agents/skills/tablecloth-verify-release/SKILL.md @@ -0,0 +1,41 @@ +--- +name: tablecloth-verify-release +description: Verify a TableCloth draft or published release across tag provenance, versions, x64 and arm64 assets, signatures, Velopack channels, release flags, and Retail follow-up automation. Use before publishing and after release. +--- + +# TableCloth 릴리스 검증 + +Draft 게시 전과 Release 게시 후에 소스, 자산, 서명, 채널과 후속 자동화를 증거로 확인합니다. + +## 소스와 버전 + +[`docs/BRANCHING.md`](../../../docs/BRANCHING.md), [`docs/RELEASE_CHANNELS.md`](../../../docs/RELEASE_CHANNELS.md), [`docs/RELEASING.md`](../../../docs/RELEASING.md)를 기준으로 다음 항목을 확인합니다. + +- Retail 태그 커밋과 `origin/main` HEAD의 일치 +- Preview 태그 커밋의 `origin/develop` 포함 여부 +- 태그 코어와 `Directory.Build.Props`의 일치 +- Preview 태그 형식과 번호의 미재사용 +- GitHub Release의 Draft와 Prerelease 플래그 + +## 자산과 서명 + +x64와 arm64에서 TableCloth 및 Spork 설치 관리자, Portable ZIP, Velopack 패키지와 채널 메타데이터, 심볼과 SBOM을 확인합니다. Retail에서는 무설치 고정 URL 자산과 Bootstrapper도 확인하고 Preview에서는 해당 별칭이 없는지 확인합니다. + +Release의 모든 `.exe`를 새 임시 디렉터리에 내려받아 Authenticode 상태를 확인합니다. Portable ZIP을 풀어 내부의 `TableCloth.exe`와 `Spork.exe`도 검사합니다. 파일 이름만으로 서명 완료를 판단하지 않습니다. + +Velopack 메타데이터는 다음 채널과 일치해야 합니다. + +- Retail TableCloth: `x64`, `arm64` +- Retail Spork: `spork-x64`, `spork-arm64` +- Preview TableCloth: `preview-x64`, `preview-arm64` +- Preview Spork: `spork-preview-x64`, `spork-preview-arm64` + +## 게시 상태와 외부 결과 + +릴리스 노트에 `UNSIGNED` 경고가 남아 있으면 게시 실패로 처리합니다. Preview는 Prerelease이며 `/releases/latest`, WinGet과 Discord에서 제외되어야 합니다. + +Retail은 정식 Release이며 `/releases/latest`가 해당 태그를 가리켜야 합니다. WinGet 워크플로 성공 뒤 실제 `microsoft/winget-pkgs` Pull Request를 확인하고 Discord 워크플로 성공 뒤 실제 공지 결과를 확인합니다. 자동화 실행 상태와 외부 결과를 구분하여 보고합니다. + +## 판정 + +검증 결과를 통과, 실패, 확인되지 않음으로 구분합니다. 실패 또는 확인되지 않은 필수 항목이 있으면 Draft를 유지하거나 게시된 Release의 영향 범위를 보고합니다. 확인하지 못한 항목을 성공으로 간주하지 않습니다. diff --git a/.agents/skills/tablecloth-verify-release/agents/openai.yaml b/.agents/skills/tablecloth-verify-release/agents/openai.yaml new file mode 100644 index 00000000..1a809b00 --- /dev/null +++ b/.agents/skills/tablecloth-verify-release/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Verify TableCloth Release" + short_description: "Verify release provenance assets and automation" + default_prompt: "Use $tablecloth-verify-release to verify this TableCloth release before and after publishing." diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc0dcd40..f2b7dccd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -361,7 +361,7 @@ jobs: # 서명하므로(앱 바이너리 + Update.exe + Setup.exe), 이미 패킹된 releases/* 만으로는 전체 서명을 # 다시 할 수 없다. 로컬이 이 페이로드를 받아 build.cmd --skip-build --sign 으로 재패킹+서명한다. # - # arm64 Native AOT 는 x64 PC 에서 빌드할 수 없어(docs/RELEASING.md §8) 빌드는 CI 의 네이티브 + # arm64 Native AOT는 x64 PC에서 빌드할 수 없으므로 빌드는 CI의 네이티브 # windows-11-arm 러너가 맡고 로컬은 서명만 담당한다. 프리뷰 레인(preview.yml)이 쓰는 것과 # 같은 계약이며, 리테일에도 같은 절차를 쓰기 위해 이름·경로를 동일하게 맞춘다. - name: Upload publish payload (for local signing) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 6adee7bf..8ed16482 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -24,7 +24,7 @@ jobs: - name: Validate version core matches tag run: | - # v1.21.0-preview.1 → 1.21.0-preview.1 → core 1.21.0 + # v1.22.0-preview.1 -> 1.22.0-preview.1 -> core 1.22.0 TAG_VERSION="${GITHUB_REF#refs/tags/v}" CORE_VERSION="${TAG_VERSION%%-preview*}" @@ -59,7 +59,8 @@ jobs: Platform: ${{ matrix.platform }} # 게시 경로는 build.cs 가 기대하는 레이아웃을 그대로 쓴다. 로컬 x64 PC 가 이 산출물을 그대로 # 내려받아 `build.cmd --skip-build --sign --preview` 로 서명 패키징할 수 있게 하기 위함이다 - # (arm64 AOT 는 로컬에서 빌드할 수 없다). 자세한 절차는 docs/RELEASING.md §8. + # (arm64 AOT 는 로컬에서 빌드할 수 없다). 자세한 절차는 docs/RELEASING.md의 + # "로컬 SimplySign 전체 서명" 절을 따른다. TableClothPublishDir: publish\Release\win-${{ matrix.platform }} SporkPublishDir: publish\spork\Release\win-${{ matrix.platform }} steps: @@ -166,9 +167,10 @@ jobs: if-no-files-found: error retention-days: 5 - # 로컬 서명용 인계 자산(docs/RELEASING.md §8). pack 이전의 게시 산출물을 그대로 넘겨, + # 로컬 서명용 인계 자산(docs/RELEASING.md의 "로컬 SimplySign 전체 서명"). + # pack 이전의 게시 산출물을 그대로 넘겨, # x64 개발 PC 가 `build.cmd --skip-build --sign --preview` 로 다시 pack 하면서 서명할 수 있게 한다. - # 서명·패키징은 아키텍처 중립이라(§3-1) arm64 페이로드도 x64 에서 패키징된다 — 로컬에서 못 하는 것은 + # 서명과 패키징은 아키텍처 중립이라 arm64 페이로드도 x64에서 패키징된다. 로컬에서 못 하는 것은 # arm64 AOT '빌드'뿐이다. 심볼 수집 단계 뒤에 올리므로 대용량 AOT pdb 는 포함되지 않는다. - name: Upload publish payload (for local signing) uses: actions/upload-artifact@v4 @@ -230,7 +232,7 @@ jobs: > **UNSIGNED — DO NOT PUBLISH** > > Preview .exe assets are not yet code-signed. Sign them locally (SimplySign) and re-upload - > before publishing this prerelease — see docs/RELEASING.md section 8. + > before publishing this prerelease. See the "Local SimplySign full signing" procedure in docs/RELEASING.md. > > **Remove this block before publishing.** (v1.21.0-preview.2 was published with it still in > place and had to be corrected afterwards; the assets were signed, only the note was wrong.) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 50df2793..cd053890 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -2,6 +2,8 @@ TableCloth는 Windows 11 이상에서 개발하도록 최적화되어 있습니다. 프로젝트 빌드 및 실행을 포함한 전체 애플리케이션 개발에는 Windows 환경이 필요합니다. 다른 운영체제에서는 문서 편집 및 카탈로그 관리와 같은 특정 작업으로 개발이 제한됩니다. +브랜치와 버전 운영은 [브랜치와 버전 관리 정책](./docs/BRANCHING.md)을 따릅니다. 다음 Minor 버전은 `develop`에서 개발하고 `X.Y.0-preview.N`으로 검증합니다. 현재 정식 버전의 긴급 패치는 `main`에서 `X.Y.Z`로 게시한 뒤 `develop`으로 순방향 전파합니다. 채널 계약과 실제 배포 절차는 [릴리스 채널](./docs/RELEASE_CHANNELS.md)과 [릴리스 실행 절차](./docs/RELEASING.md)에서 확인할 수 있습니다. + > [!WARNING] > Windows 10은 2025년 12월부로 지원이 중단된 OS이며 보안 업데이트가 더 이상 제공되지 않습니다. 개발이나 빌드가 가능하더라도 보안상 권장하지 않습니다. diff --git a/Directory.Build.Props b/Directory.Build.Props index 66ee9163..070564dd 100644 --- a/Directory.Build.Props +++ b/Directory.Build.Props @@ -7,7 +7,7 @@ 1 21 - 0 + 1 0 @@ -37,4 +37,4 @@ - \ No newline at end of file + diff --git a/docs/AVALONIA_AOT_MIGRATION.md b/docs/AVALONIA_AOT_MIGRATION.md index a12beb5e..84d2d3c4 100644 --- a/docs/AVALONIA_AOT_MIGRATION.md +++ b/docs/AVALONIA_AOT_MIGRATION.md @@ -1,7 +1,8 @@ -# WPF → Avalonia + Native AOT 마이그레이션 계획 (이슈 #296) +# WPF에서 Avalonia와 Native AOT로 전환한 기록 (이슈 #296) -> 상태: **계획 수립 + AOT 호환성 실측 완료** (2026-07-24) -> 대상 이슈: [#296](https://github.com/yourtablecloth/TableCloth/issues/296) — Avalonia + Native AOT 기반 UI 전환 +> 상태: **v1.21.0 정식 출시로 전환 완료** (2026-08-23) +> 이 문서는 설계와 구현 당시의 판단을 보존하는 이력 문서입니다. 현재 릴리스 정책은 [RELEASE_CHANNELS.md](RELEASE_CHANNELS.md)와 [BRANCHING.md](BRANCHING.md)를 따릅니다. +> 대상 이슈: [#296](https://github.com/yourtablecloth/TableCloth/issues/296), Avalonia와 Native AOT 기반 UI 전환 > 선행 참고: [TableClothVNext](https://github.com/yourtablecloth/TableClothVNext) (Avalonia 재작성 시도 아카이브) ## 1. 목표와 성공 기준 diff --git a/docs/BRANCHING.md b/docs/BRANCHING.md new file mode 100644 index 00000000..cb5cfd4f --- /dev/null +++ b/docs/BRANCHING.md @@ -0,0 +1,116 @@ +# TableCloth 브랜치와 버전 관리 정책 + +TableCloth는 2026년 8월 23일에 게시한 v1.21.0부터 `main`과 `develop`을 중심으로 정식 버전과 다음 Minor 버전을 병행합니다. 현재 정식 버전의 호환성 패치는 `X.Y.Z`로 게시하고 다음 기능 개발은 `X.(Y+1).0-preview.N`으로 검증합니다. + +이 문서는 브랜치의 역할, 버전 증가 기준, 핫픽스 전파 방식과 정식 승격 조건을 다룹니다. 채널별 배포 계약은 [RELEASE_CHANNELS.md](RELEASE_CHANNELS.md), 실제 게시 명령은 [RELEASING.md](RELEASING.md)에서 이어집니다. + +> 기준일: 2026년 8월 23일. CI와 배포 계약을 변경하면 이 문서와 저장소 스킬을 같은 변경에서 갱신합니다. + +## 정식 버전과 다음 버전의 병행 개발 + +브랜치별 책임은 다음과 같이 고정합니다. + +| 브랜치 | 책임 | 버전 예시 | 배포 대상 | +| --- | --- | --- | --- | +| `main` | 최신 정식 버전과 현재 지원 패치 | `1.21.0`, `1.21.1` | Retail | +| `develop` | 다음 Minor 버전의 기능 개발과 Preview | `1.22.0-preview.1` | Preview | +| `hotfix/X.Y.Z` | 현재 정식 버전의 긴급 수정 | `1.21.1` | 검증 후 `main`으로 병합 | +| `release/X.Y` | 이전 Minor 버전의 추가 유지보수 | `1.21.x` | 장기 지원이 필요할 때만 생성 | + +`main`에는 언제든 배포할 수 있는 상태만 둡니다. `develop`은 다음 Minor 버전의 통합 지점으로 사용합니다. 기능 브랜치는 `develop`을 기준으로 만들고 검증을 마치면 `develop`으로 병합합니다. + +`release/X.Y`는 `main`이 다음 Minor 버전으로 이동한 뒤에도 이전 버전을 지원할 때만 만듭니다. 최신 정식 버전만 지원한다면 장기 유지 브랜치를 만들지 않습니다. + +## 버전 번호 증가 기준 + +[Semantic Versioning 2.0.0](https://semver.org/)을 기준으로 버전을 결정합니다. + +- `PATCH`: 기존 호환성을 유지하는 결함 수정과 긴급 패치 +- `MINOR`: 기존 호환성을 유지하는 기능 추가와 동작 개선 +- `MAJOR`: 호환되지 않는 공개 계약 변경 +- `preview.N`: 다음 정식 버전을 앞서 검증하는 순차 Preview + +버전의 단일 출처는 [`Directory.Build.Props`](../Directory.Build.Props)입니다. 정식 버전과 Preview 모두 `Major.Minor.Patch`를 이 파일에 기록하고 `Revision`은 `0`으로 유지합니다. Preview 식별자는 파일에 넣지 않고 태그와 패키지 버전에만 추가합니다. + +태그는 다음 형식을 사용합니다. + +```text +v1.21.1 +v1.22.0-preview.1 +v1.22.0-preview.2 +``` + +Preview 번호는 1부터 증가하며 이미 게시했거나 삭제한 번호를 재사용하지 않습니다. `preview.10`처럼 숫자 식별자를 점으로 구분하면 SemVer가 번호를 숫자로 비교합니다. + +## 다음 Minor 버전의 시작 + +v1.21.0을 게시한 뒤 v1.22.0 개발을 시작하는 흐름은 다음 순서를 따릅니다. + +1. `main`과 원격 태그가 최신 상태인지 확인합니다. +2. 최신 `main`에서 `develop`을 만들거나 기존 `develop`의 이력을 검토합니다. +3. `Directory.Build.Props`를 `1.22.0.0`으로 갱신합니다. +4. 전체 빌드와 단위 테스트를 실행합니다. +5. 기능 브랜치를 `develop`에서 분기합니다. +6. 검증할 시점마다 `v1.22.0-preview.N` 태그를 생성합니다. + +다음 Minor 버전의 버전 변경 커밋에는 기능 변경을 섞지 않습니다. 버전 변경 이력을 분리하면 현재 정식 버전의 패치를 `develop`으로 옮길 때 충돌 범위를 줄일 수 있습니다. + +## 현재 정식 버전의 핫픽스 + +긴급 패치는 최신 `main`에서 `hotfix/X.Y.Z`를 만들어 처리합니다. 패치 코드와 버전 변경을 별도 커밋으로 나누면 다음 버전 브랜치에 코드만 전파할 수 있습니다. + +```text +main 1.21.0 + └─ hotfix/1.21.1 + ├─ 수정과 테스트 + └─ main 병합 및 v1.21.1 태그 + └─ 수정 커밋을 develop 1.22.0으로 순방향 전파 +``` + +`main`에서 Retail 릴리스를 마친 뒤 수정 커밋을 `develop`으로 즉시 체리픽하거나 병합합니다. `develop`의 버전은 목표 버전인 `1.22.0`을 유지합니다. 전파 과정에서 `Directory.Build.Props`가 `1.21.1`로 되돌아가지 않도록 확인합니다. + +동일한 문제를 두 브랜치에서 따로 수정하지 않습니다. Retail 패치 커밋을 다음 버전으로 전파하고 동일한 테스트로 회귀를 막습니다. + +## Preview의 정식 승격 + +`develop`을 정식 버전으로 승격하는 흐름은 다음 순서를 따릅니다. + +1. 새 기능 병합을 중지하고 승격 범위를 고정합니다. +2. 최신 `main`의 Retail 패치를 `develop`에 반영합니다. +3. 필요하면 마지막 Preview 또는 Release Candidate를 게시합니다. +4. 전체 테스트를 완료하고 TableCloth 호스트 시나리오는 호스트 Windows에서 스모크 테스트합니다. Spork 게스트 시나리오만 Windows Sandbox 안에서 확인합니다. +5. `develop`을 `main`으로 병합합니다. +6. 새 `main` HEAD에 `vX.Y.0` 정식 태그를 생성합니다. +7. CI Draft, 로컬 서명, 자산 검증과 정식 게시를 완료합니다. +8. 다음 Minor 버전 개발을 시작할 때 `develop`의 버전을 다시 올립니다. + +정식 태그는 병합을 마친 `main` HEAD만 가리킵니다. Preview 태그는 `develop` 이력에 포함된 커밋만 가리킵니다. + +## 보호 규칙과 검증 경계 + +GitHub의 [보호된 브랜치](https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches)는 `main`과 `develop`에 다음 조건을 적용하는 데 사용할 수 있습니다. + +- Pull Request를 통한 병합 +- x64와 arm64 빌드 및 테스트 성공 +- 병합 전 최신 대상 브랜치 반영 +- 강제 Push와 태그 이동 차단 + +현재 릴리스 워크플로는 태그와 `Directory.Build.Props`의 버전 일치 여부를 확인하지만 태그의 브랜치 소속까지 강제하지 않습니다. 자동 검증을 추가하기 전까지 릴리스 담당자와 저장소 스킬이 다음 조건을 검사합니다. + +- Retail 태그 커밋과 `origin/main` HEAD의 일치 +- Preview 태그 커밋의 `origin/develop` 포함 여부 +- `^v[0-9]+\.[0-9]+\.[0-9]+-preview\.[1-9][0-9]*$` 형식 + +## 저장소 릴리스 스킬 + +`.agents/skills`에는 이 정책을 실행하는 스킬을 버전 관리합니다. + +- [`tablecloth-start-next-version`](../.agents/skills/tablecloth-start-next-version/SKILL.md): 다음 Minor 버전 개발 시작 +- [`tablecloth-release-preview`](../.agents/skills/tablecloth-release-preview/SKILL.md): Preview 태그, Draft, 서명과 게시 +- [`tablecloth-release-hotfix`](../.agents/skills/tablecloth-release-hotfix/SKILL.md): Retail 핫픽스와 `develop` 순방향 전파 +- [`tablecloth-promote-stable`](../.agents/skills/tablecloth-promote-stable/SKILL.md): `develop`의 정식 승격 +- [`tablecloth-sign-release`](../.agents/skills/tablecloth-sign-release/SKILL.md): SimplySign 기반 로컬 전체 서명 +- [`tablecloth-verify-release`](../.agents/skills/tablecloth-verify-release/SKILL.md): 게시 전후 자산과 후속 자동화 검증 +- [`tablecloth-release`](../.agents/skills/tablecloth-release/SKILL.md): 릴리스 유형 판단과 하위 스킬 조율 + +Claude용 `.claude/skills`는 `.agents/skills`를 가리키는 심볼릭 링크로 유지합니다. 두 도구의 스킬 내용을 따로 복제하지 않습니다. diff --git a/docs/RELEASE_CHANNELS.md b/docs/RELEASE_CHANNELS.md index 8c8541bd..49102c10 100644 --- a/docs/RELEASE_CHANNELS.md +++ b/docs/RELEASE_CHANNELS.md @@ -1,172 +1,84 @@ -# Retail / Preview 릴리스 채널 분리와 승격 기록 (이슈 #296) +# TableCloth Retail과 Preview 릴리스 채널 -> 상태: **Preview 구현과 검증 완료, Retail 승격 반영** · 2026-08-23 -> 배경: WPF→Avalonia+Native AOT(이슈 [#296](https://github.com/yourtablecloth/TableCloth/issues/296)) 전환은 변화 폭이 커서, -> 안정 사용자를 보호한 채 조기 검증을 받기 위해 **Retail(안정)** 과 **Preview(선행)** 두 릴리스 링을 분리했다. -> `v1.21.0-preview.1`부터 `.3`까지 별도 Preview 파이프라인으로 검증했으며, `main` 통합부터 Retail도 -> Avalonia+Native AOT로 게시한다. 다음 내용은 초기 설계와 승격 결정을 함께 보존한다. -> 운영 절차: [RELEASING.md](RELEASING.md), 마이그레이션 기록: [AVALONIA_AOT_MIGRATION.md](AVALONIA_AOT_MIGRATION.md). +TableCloth는 Retail과 Preview 두 릴리스 링을 운영합니다. Retail은 기본 업데이트 채널로 최신 정식 버전을 제공하고 Preview는 사용자가 선택한 경우에만 다음 Minor 버전의 선행 빌드를 제공합니다. -## 1. 초기 확정 결정 (2026-07-25) +이 문서는 각 채널의 버전, GitHub Release, Velopack 메타데이터와 외부 배포 계약을 정리합니다. 브랜치와 버전 관리 규칙은 [BRANCHING.md](BRANCHING.md), 게시 절차는 [RELEASING.md](RELEASING.md)에서 다룹니다. -| 항목 | 결정 | 근거 | -| ---- | ---- | ---- | -| 채널 매핑 | **Retail = 현행 WPF 유지 / Preview = AOT 신규 레인** | 기존 사용자는 안정 WPF 를 계속 받고, AOT 는 opt-in 으로 조기 검증. AOT 안정화 후 Retail 로 승격(§9). 리스크 최소·되돌리기 용이. | -| Preview 배포 | **별도 프리릴리스 설치본 + Velopack `preview` 채널** | 프리뷰 설치본을 한 번 받으면 이후 프리뷰 채널로 자동 업데이트. Retail·winget·무설치 웹앱은 GitHub 프리릴리스 특성상 자동으로 무영향. Velopack 네이티브 방식이라 가장 단순·견고. | -| 진행 방식 | **설계 문서 우선, 구현은 승인 후 단계별** | 외부 계약(winget/웹앱), CI, 버전 체계를 문서로 확정한 뒤 이관. | +> 기준일: 2026년 8월 23일. v1.21.0부터 Retail과 Preview 모두 Avalonia와 Native AOT 빌드를 사용합니다. -## 2. 현행 구조 요약 (분리 전) +## 두 릴리스 링의 역할 -- **버전 단일 출처**: `Directory.Build.Props`(4-part 숫자). CI `validate-version` 이 태그의 3-part 와 비교. -- **릴리스**: 태그 `v*` push → `build.yml`(x64/arm64 빌드 + **미서명 draft**) → 로컬 `build.cmd --sign` → `gh release upload --clobber` → UNSIGNED 마커 제거 후 Publish → `released` → winget PR. -- **Velopack 채널**: TableCloth = ``(x64/arm64), Spork = `spork-`. **링 개념 없음**. -- **자산명**: `TableCloth_<4part>_Release_.exe` / `_Portable.zip`, Spork 동일 패턴. -- **업데이트**(`AppUpdateManager`): ① Velopack(채널 미지정=기본) → ② 실패 시 GitHub API `/releases/latest` 에서 `_Release_.exe` 매칭. `/releases/latest` 는 **프리릴리스를 제외**한 최신을 반환. -- **외부 고정 URL 계약**: winget(설치 관리자 Setup.exe), 무설치 웹앱(`releases/latest/download/{no-install-spork.wsb, SporkBootstrap_.exe, Spork__Portable.zip}`). 모두 `/releases/latest`(=Retail) 에 의존. +| 항목 | Retail | Preview | +| --- | --- | --- | +| 소스 | `main` | `develop` | +| 버전 | `X.Y.Z` | `X.(Y+1).0-preview.N` | +| GitHub 상태 | 정식 Release | Prerelease | +| 앱 기본값 | 기본 선택 | 사용자 옵트인 | +| WinGet | 정식 게시 후 자동 제출 | 제출하지 않음 | +| Discord | 정식 게시 후 자동 공지 | 공지하지 않음 | +| `/releases/latest` | 최신 Retail을 가리킴 | 대상에서 제외 | -> **핵심 제약(분리의 실질적 계기):** `build.yml` 은 아직 WPF 게시 플래그(`PublishSingleFile`+`PublishReadyToRun`)를 명시한다. 이는 M5 의 -> `PublishAot` 과 **상호배타**다. 따라서 AOT 는 현행 Retail CI 로 내보낼 수 없고 **전용 게시 경로**가 필요하다 → Preview 레인이 이를 담당. +GitHub는 Prerelease를 최신 정식 Release와 구분합니다. 저장소의 WinGet과 Discord 워크플로도 `release: released` 이벤트만 처리하므로 Preview 게시에서는 실행되지 않습니다. -## 3. 채널 모델 +## Velopack 채널 매핑 -``` -Retail (안정, 대다수) Preview (선행, opt-in) -├─ 소스: main (WPF v1.20.x) ├─ 소스: feature/avalonia-aot (AOT) -├─ GitHub: 정식 릴리스 ├─ GitHub: 프리릴리스(prerelease=true) -│ (/releases/latest 대상) │ (/releases/latest 에서 제외됨) -├─ Velopack 채널: ├─ Velopack 채널: preview- -├─ 자산: TableCloth_…_x64.exe ├─ 자산: TableCloth-Preview_…_x64.exe -├─ winget: ✅ 제출 ├─ winget: ❌ (프리릴리스라 released 미발생) -└─ 무설치 웹앱: ✅ (latest) └─ 무설치 웹앱: ❌ (latest 아님) -``` - -- **Retail 은 현행과 100% 동일**하게 유지한다(채널명·자산명·CI·winget·웹앱 무변경). 기존 설치 사용자의 자동 업데이트가 끊기지 않는다. -- **Preview 는 순수 추가(additive)** 레인. Retail 을 건드리지 않는다. -- GitHub 의 **prerelease 플래그**가 분리의 중심축: 프리릴리스는 `/releases/latest` 에서 빠지므로 winget·무설치 웹앱·Retail 업데이트 폴백이 **자동으로 Preview 를 무시**한다. - -## 4. Velopack 채널 매핑 - -| 앱 | Retail 채널 | Preview 채널 | -| ---- | ---- | ---- | -| TableCloth | `x64` / `arm64` (현행) | `preview-x64` / `preview-arm64` | -| Spork | `spork-x64` / `spork-arm64` (현행) | `spork-preview-x64` / `spork-preview-arm64` | - -- Velopack 메타데이터(`releases..json` / `RELEASES-` / `assets..json`)가 채널별로 분리되어 같은 릴리스 자산 폴더에 공존해도 충돌하지 않는다(현행 arch 분리와 동일 원리). -- Preview 설치본은 설치 시 자신의 채널(`preview-`)을 각인하고, 앱의 `UpdateManager` 가 그 채널 메타데이터로 업데이트를 확인한다(§8). - -## 5. 버전 체계 - -- **Directory.Build.Props 는 4-part 숫자 유지**(AssemblyVersion/FileVersion 용). 여기에 `-preview` 접미사를 넣지 않는다. -- **Preview 표시 버전은 SemVer2 프리릴리스**: `X.Y.Z-preview.N`. - - `X.Y.Z` = 다음 목표 Retail 버전(예: 현재 Retail 1.20.6 → Preview 는 `1.21.0-preview.*`). - - `N` = 프리뷰 반복 번호(태그에서 부여, 예: `v1.21.0-preview.3`). -- **Velopack `--packVersion`** 은 SemVer2 프리릴리스를 그대로 받는다(`1.21.0-preview.3`). Velopack 이 프리릴리스를 정상 순서 비교하므로 프리뷰 간 자동 업데이트가 동작한다. -- **태그 검증(`validate-version`) 확장**: `vX.Y.Z-preview.N` 태그는 3-part 코어(`X.Y.Z`)만 Props 와 비교하고 `-preview.N` 접미사는 검증에서 제외(정규식으로 코어 추출). Retail 태그(`vX.Y.Z`)는 현행 그대로. +[Velopack 릴리스 채널](https://docs.velopack.io/packaging/channels)은 채널별 업데이트 메타데이터를 분리합니다. TableCloth는 제품과 CPU 아키텍처를 다음과 같이 매핑합니다. -## 6. 자산명 규칙 (Preview) +| 애플리케이션 | Retail | Preview | +| --- | --- | --- | +| TableCloth x64 | `x64` | `preview-x64` | +| TableCloth arm64 | `arm64` | `preview-arm64` | +| Spork x64 | `spork-x64` | `spork-preview-x64` | +| Spork arm64 | `spork-arm64` | `spork-preview-arm64` | -Retail 과 이름이 겹치지 않도록 `-Preview` 를 접두 삽입한다(웹/자동화가 링을 이름으로 구분 가능): +앱의 `AppUpdateManager`는 사용자가 선택한 릴리스 링과 현재 아키텍처를 조합하여 명시적인 채널을 조회합니다. Retail은 GitHub `/releases/latest`를 폴백으로 사용하고 Preview는 Prerelease 목록에서 Preview 자산을 찾습니다. -| 종류 | Retail | Preview | -| ---- | ---- | ---- | -| 설치 관리자 | `TableCloth__Release_.exe` | `TableCloth-Preview__Release_.exe` | -| 포터블 | `TableCloth__Release__Portable.zip` | `TableCloth-Preview__Release__Portable.zip` | -| Spork | `Spork_…` | `Spork-Preview_…` | +## 버전과 자산 이름 -- `` 는 Preview 에서도 4-part 파일 버전 문자열을 쓰되(파일명 안정), 릴리스/Velopack 표시는 §5 의 SemVer 프리릴리스. -- **무설치 고정 URL 별칭(`SporkBootstrap_.exe`, `Spork__Portable.zip`, `no-install-spork.wsb`)은 Preview 레인에서 생성하지 않는다.** 이들은 `latest/download`(=Retail) 계약이므로 프리릴리스에 얹으면 혼동만 준다. +Preview는 [Semantic Versioning 2.0.0](https://semver.org/)의 점으로 구분한 숫자 식별자를 사용합니다. -## 7. GitHub 릴리스 전략 - -- **Retail**: 현행 그대로 — draft 로 만들고 서명 후 **정식 릴리스로 Publish**(prerelease=false) → `released` → winget. -- **Preview**: **prerelease=true 로 생성·게시**. - - `/releases/latest` 에서 제외 → winget·무설치 웹앱·Retail 폴백이 자동으로 무시. - - 서명: Preview 도 동일 서명 절차 권장(사용자 실행 신뢰). 미서명 배포를 허용할지는 §14 참조. - - 릴리스 노트: "미리 보기 — 실사용 검증용, 문제 보고 환영" 배너 + AOT 변경 요약. - -## 8. 앱 측 채널 인식 - -사용자는 앱 옵션에서 업데이트 채널을 선택한다. 기본값은 Retail이며 선택값을 preferences 파일에 보존한다. - -- **채널 모델**: `ReleaseChannel` enum의 `Retail`과 `Preview` 값을 옵션 UI와 업데이트 관리자가 함께 참조한다. -- **업데이트(`AppUpdateManager`)**: - - Velopack: **양쪽 링 모두 `ExplicitChannel` 을 명시**한다(Retail = ``, Preview = `preview-` — `vpk pack --channel` 값과 같은 이름). 비워 두면 Velopack 기본값이 "설치 시점에 구워진 채널"이라, Preview 설치본에서 Retail 을 골라도 전환이 걸리지 않는다. 소스는 Preview 만 `prerelease:true`. - - GitHub API 폴백: Preview 는 `/releases/latest` 대신 `/releases`(프리릴리스 포함)에서 최신 prerelease 를 골라 `TableCloth-Preview_…_.exe` 매칭. Retail 은 현행(`/releases/latest` + `_Release_`). - - **되돌리기(Preview → Retail)**: `AllowVersionDowngrade` 를 켠다. 대상 버전이 현재보다 낮기 때문(예: `1.21.0-preview.1` → `1.20.9`)이며, 이 옵션이 없으면 Velopack 이 "업데이트 없음"으로 판단한다. 안정 링에 머무는 평상시에는 켜지 않는다(의도치 않은 하향 방지). - - 설치 정보(IsInstalled/CurrentVersion)는 채널 무관하므로 기본 매니저 유지. -- **설정 영속성**: 채널 선택은 preferences 파일(사용자 데이터 위치)에 저장되어 업데이트 후에도 유지되어야 한다(§14 검증 항목). -- **되돌리기는 수동 절차로 안내한다(자동화하지 않음)**: 설정 파일이 재설치를 살아남기 때문에, 안정 버전을 먼저 설치해도 설정이 계속 Preview 를 가리켜 다시 끌려 올라간다. 이를 앱이 자동으로 판정하는 방안(설치본의 링을 관측해 설정을 따라가게 하는 대조 로직)과 인앱 다운그레이드(`AllowVersionDowngrade`)를 함께 시도했으나 **의도대로 동작하지 않아 들어냈다**. 자동 판정이 어려운 근본 이유는 *미리 보기로 막 전환한 사용자*(설정=Preview, 설치본=Retail)와 *되돌리려는 사용자*(설정=Preview, 설치본=Retail)가 **겉보기에 같은 상태**라서다. 잘못 구분하면 방금 켠 미리 보기 설정이 저절로 꺼지는 쪽이 되어 더 나쁘다. - - 대신 **순서를 사용자가 정하도록** 안내한다: ① 채널을 Retail 로 바꿔 Preview 링에서 빠져나온 뒤 ② 안정 버전 설치 파일을 직접 받아 설치. 절차는 [TROUBLESHOOTING_UPDATE_CHANNEL.md](TROUBLESHOOTING_UPDATE_CHANNEL.md), 같은 취지의 문구가 옵션 창의 미리 보기 경고에도 들어간다. - - ①이 실제로 효력을 가지려면 위 `ExplicitChannel` 명시가 필요하다. 그것이 없으면 Retail 을 골라도 조회는 계속 `preview-` 로 가서 옵트아웃 자체가 무의미해진다. -- **UI 표시**: 옵션의 미리 보기 탭에서 Retail과 Preview를 선택하고 Preview 채널의 특성과 되돌리기 순서를 안내한다. - -## 9. AOT의 Retail 승격 결과 - -AOT는 세 차례 Preview 릴리스에서 x64와 arm64 빌드 및 릴리스 생성을 검증한 뒤 2026년 8월 23일 `main` 통합을 시작했다. -승격 작업은 다음 순서로 반영했다. +```text +Retail: v1.21.1 +Preview: v1.22.0-preview.1 + v1.22.0-preview.2 +``` -1. `v1.21.x`를 `main` 통합 브랜치에 병합하고 WPF 파일을 제거한다. -2. `build.yml` Retail 레인을 AOT 게시로 전환하고 WPF 게시 플래그를 제거한다. -3. **하위호환(중요)**: 기존 Preview 설치 사용자는 채널 `preview-` 에 묶여 있다. 승격 시 **최소 한 번은 승격 버전을 `preview-` 채널에도 게시**해 프리뷰 사용자를 Retail 로 유도하거나, 앱이 승격 감지 후 채널을 Retail 로 전환하는 마이그레이션을 둔다. (Velopack 채널 전환은 자동이 아니므로 브리지 필요.) -4. winget/무설치 웹앱은 Retail 이 AOT 로 바뀌어도 자산명·고정 URL 계약이 동일하면 무영향. +`Directory.Build.Props`에는 `1.22.0` 코어만 기록합니다. `preview.N`은 태그와 Velopack 패키지 버전에 추가합니다. -> Preview 채널 브리지는 v1.21.0 정식 게시 과정에서 최종 적용한다. §3부터 §8까지는 승격 전 병행 운영 기록이다. +Retail 자산은 기존 고정 URL 계약을 유지합니다. Preview 자산은 이름에 `-Preview`를 넣어 정식 자산과 구분합니다. -## 10. 외부 계약 영향 요약 +- Retail: `TableCloth__Release_.exe` +- Preview: `TableCloth-Preview__Release_.exe` +- Retail: `Spork__Release_.exe` +- Preview: `Spork-Preview__Release_.exe` -| 대상 | 영향 | 대응 | -| ---- | ---- | ---- | -| **winget** | 없음 | 프리릴리스라 `released` 미발생 → 자동 제외. Retail 만 계속 제출. | -| **무설치 웹앱**(yourtablecloth.app) | 없음 | `latest/download` 는 최신 정식 릴리스만 → Preview(프리릴리스) 미노출. Preview 레인은 고정 URL 별칭을 만들지 않음(§6). | -| **후원자/기여자 파이프라인** | 없음 | 릴리스와 무관. | -| **SBOM/attestation** | Preview 도 생성 권장 | CI Preview 레인에 동일 스텝(선택). | +Preview는 `SporkBootstrap_.exe`, `Spork__Portable.zip`, `no-install-spork.wsb`와 같은 `/releases/latest/download` 고정 URL 별칭을 만들지 않습니다. -## 11. CI 구현 +## Preview 사용자의 정식 버전 전환 -Preview는 `.github/workflows/preview.yml`이 담당하고 Retail은 `.github/workflows/build.yml`이 담당한다. +Preview와 Retail은 서로 다른 Velopack 메타데이터를 사용합니다. 따라서 Preview 사용자가 같은 버전의 Retail 패키지를 자동으로 받는다고 가정하지 않습니다. -- **트리거**: Preview 태그 규칙 `v*-preview.*`(예: `v1.21.0-preview.3`). Retail 은 현행 `v*`(프리릴리스 접미사 없음). - - `validate-version`: 태그에서 3-part 코어 추출 후 Props 와 비교(§5). Preview 접미사 허용. -- **빌드/게시**: 두 레인 모두 **AOT 게시**(`dotnet publish -r win-` → csproj가 `PublishAot` 자동 활성화). WPF 전용 `PublishSingleFile`과 `ReadyToRun` 플래그는 전달하지 않는다. x64는 `windows-latest`, arm64는 `windows-11-arm` 네이티브 러너에서 빌드한다. -- **패키징**: `vpk pack --channel preview-`(TableCloth) / `spork-preview-`(Spork). pdb 는 심볼 자산으로 분리(현행과 동일). -- **자산명**: §6 규칙(`TableCloth-Preview_…`). -- **릴리스**: `create-release` 를 **prerelease=true** 로(Preview 태그일 때). 무설치 고정 URL 별칭 스텝은 Preview 에서 건너뜀. -- **winget/discord**: `released` 트리거라 Preview(프리릴리스)에서는 발생하지 않음(무변경). +현재 정책은 Preview를 계속 사용하는 옵트인 링으로 유지합니다. Preview 사용자가 Retail로 돌아가려면 앱 설정에서 업데이트 채널을 Retail로 바꾼 뒤 정식 설치 관리자를 직접 설치합니다. 앱이 더 낮은 Retail 버전으로 자동 다운그레이드하지 않습니다. 실제 사용자 절차는 [TROUBLESHOOTING_UPDATE_CHANNEL.md](TROUBLESHOOTING_UPDATE_CHANNEL.md)에 기록합니다. -## 12. AOT 게시 플래그 +Preview 사용자를 정식 버전으로 자동 이동하는 기능을 도입하려면 정식 패키지를 Preview 채널 메타데이터에도 노출하는 승격 브리지를 별도로 구현하고 검증합니다. 해당 브리지를 구현하기 전에는 자동 승격을 릴리스 조건으로 기록하지 않습니다. -- `PublishSingleFile`과 `PublishReadyToRun`은 `PublishAot`과 배타이므로 Retail 및 Preview 워크플로에서 전달하지 않는다. -- 각 진입점 csproj의 RID 조건부 그룹이 `PublishAot=true`와 크기 최적화 속성을 적용한다. -- 두 워크플로는 `dotnet publish -r win- -c <구성>` 형식으로 AOT 산출물을 생성한다. +## 정식 승격과 다음 Preview -## 13. build.cs (로컬 서명 빌드) +`v1.22.0-preview.N`을 검증한 뒤 `develop`을 `main`으로 병합하고 `v1.22.0`을 Retail로 게시합니다. 정식 게시 자산은 Retail 채널에만 들어갑니다. -- `--preview` 플래그 추가: 설정 시 채널(`preview-`/`spork-preview-`), 자산명(`-Preview`), 버전(SemVer 프리릴리스), 무설치 별칭 생략을 일괄 적용. -- 서명 경로(`--sign`)는 Retail/Preview 공통(SimplySign). AOT 네이티브 exe 서명은 signtool 로 동일 적용(별도 검증 필요 — §14). -- `EnsureVsWhereOnPath`/`PruneSymbols`(M5)는 Preview(AOT)에서 그대로 활용. +정식 게시 후 다음 기능 개발을 시작할 때 `develop`의 버전을 `1.23.0`으로 올리고 Preview 번호를 다시 1부터 시작합니다. 이전 Preview 번호와 패키지를 새 버전 코어에서 이어서 사용하지 않습니다. -## 14. 잔여 검토 항목 +## 외부 배포 계약 -- **arm64 Preview(AOT)**: `windows-11-arm` 네이티브 러너에서 빌드하고 x64 서명 호스트에서 패키징 및 서명하는 경로를 검증했다. -- **Preview 서명**: CI 게시 산출물을 x64 서명 호스트로 인계해 TableCloth와 Spork의 x64 및 arm64 패키지를 서명한다. -- **Preview 앱 내 채널 전환**: 옵션의 업데이트 채널 설정으로 Retail과 Preview를 선택한다. -- **승격 시 Velopack 채널 브리지**(§9-3)의 구체 방법: 승격 시점에 확정. -- **Preview 버전 `N` 부여 주체**: 태그 수기 vs CI 자동 카운터. 초기엔 태그 수기(`-preview.N`) 권장. +Retail 정식 게시가 발생하면 다음 자동화가 이어집니다. -## 15. 구현 결과 +- [WinGet 제출 워크플로](../.github/workflows/winget_publish.yml)의 `microsoft/winget-pkgs` Pull Request 생성 +- [Discord 공지 워크플로](../.github/workflows/discord_release.yml)의 정식 출시 알림 +- `/releases/latest/download`를 사용하는 무설치 실행 자산 갱신 +- Retail 앱의 자동 업데이트 메타데이터 갱신 -1. **앱 채널 인식**: `ReleaseChannel` 설정과 `AppUpdateManager`의 Velopack 및 GitHub 프리릴리스 분기 구현. -2. **build.cs `--preview`**: 프리뷰 채널, 자산명, 버전, 고정 URL 별칭 생략 처리 구현. -3. **CI Preview 레인**: `preview.yml`에 프리뷰 태그 검증, AOT 게시, prerelease 생성, 서명용 게시 산출물 인계 구현. -4. **문서**: `RELEASING.md`에 Preview 릴리스 런북과 서명 절차 반영. -5. **arm64 Preview**: `windows-11-arm` 네이티브 빌드와 x64 호스트 서명 경로 검증. -6. **Retail 승격**: `v1.21.x` 통합과 Retail AOT 워크플로 전환. +Preview는 위 자동화에서 제외됩니다. Preview를 정식 Release로 잘못 게시하거나 `latest`로 지정하지 않습니다. -## 16. 주요 반영 파일 +## Avalonia와 Native AOT 전환 기록 -- `src/TableCloth.Core` 또는 `src/Shared`: `ReleaseChannel` 상수/헬퍼. -- `src/TableCloth.App/Components/Implementations/AppUpdateManager.cs`: 채널 분기 + 프리릴리스 폴백. -- `src/TableCloth.App` About/Splash 뷰: Preview 배지. -- `build.cs`: `--preview` 모드. -- `.github/workflows/build.yml`: Preview 레인 + `validate-version` 프리릴리스 허용. -- `docs/RELEASING.md`: Preview 런북. 본 문서. +v1.20에서 v1.21로 이어진 WPF 제거, Avalonia 이관과 Native AOT 검증은 [AVALONIA_AOT_MIGRATION.md](AVALONIA_AOT_MIGRATION.md)에 보존합니다. 현재 채널 정책은 두 링 모두 Avalonia와 Native AOT를 사용한다는 전제에서 운영합니다. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index eeb48436..914c72fe 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,312 +1,229 @@ -# TableCloth 릴리스 절차 (Runbook) +# TableCloth 릴리스 실행 절차 -새 버전을 출시할 때 따르는 단계별 절차입니다. 핵심 흐름: +TableCloth의 Retail과 Preview 릴리스는 태그 Push로 CI 빌드를 시작하고 미서명 Draft를 생성합니다. 릴리스 담당자는 이 PC의 SimplySign 인증서로 CI 게시 산출물을 다시 패키징하고 서명한 뒤 Draft 자산을 교체합니다. -> 버전 bump → 태그 push(CI가 미서명 draft 생성) → `build.cmd --sign`(로컬 전체 서명) → `gh release upload --clobber` → `UNSIGNED` 마커 제거 후 Publish → winget 자동 PR 확인 +이 문서는 릴리스 유형 선택부터 태그 검증, 로컬 서명, 게시와 후속 자동화 확인까지 다룹니다. 브랜치 수명 주기는 [BRANCHING.md](BRANCHING.md), 채널별 동작은 [RELEASE_CHANNELS.md](RELEASE_CHANNELS.md)에서 설명합니다. -관련 구성요소: [`.github/workflows/build.yml`](../.github/workflows/build.yml) (빌드+draft), [`build.cs`](../build.cs)/[`build.cmd`](../build.cmd) (로컬 빌드+서명), [`.github/workflows/winget_publish.yml`](../.github/workflows/winget_publish.yml) + [`tools/winget/submit-winget.cs`](../tools/winget/submit-winget.cs) (winget 자동 제출), [`Directory.Build.Props`](../Directory.Build.Props) (버전 단일 출처). +> 기준일: 2026년 8월 23일. x64와 arm64 Native AOT 빌드는 GitHub Actions의 각 네이티브 러너가 담당하고 로컬 x64 PC는 두 아키텍처의 패키징과 Authenticode 서명을 담당합니다. -> **릴리스 채널(Retail/Preview):** 이 런북은 **Retail(안정)** 정식 릴리스 절차다. Avalonia+Native AOT 전환처럼 변화 폭이 큰 -> 릴리스는 **Preview** 링으로 먼저 프리릴리스(prerelease)로 낸다 — GitHub 프리릴리스라 `/releases/latest`·winget·무설치 -> 웹앱은 자동으로 Retail 만 가리키고, 사용자는 앱 옵션(미리 보기 탭 → 업데이트 채널)에서 미리 보기로 전환해 받는다. -> v1.20.7 부터 이 in-app 채널 토글이 탑재된다. 채널 설계·자산명·버전(`X.Y.Z-preview.N`)·CI 규칙은 -> [RELEASE_CHANNELS.md](RELEASE_CHANNELS.md) 참조. Preview 게시 절차는 §8에서 다룬다. +## 릴리스 유형 선택 ---- +| 목적 | 소스 | 버전과 태그 | GitHub 상태 | 후속 자동화 | +| --- | --- | --- | --- | --- | +| 현재 정식 버전 패치 | `main` | `v1.21.1` | 정식 Release | WinGet, Discord | +| 다음 버전 선행 검증 | `develop` | `v1.22.0-preview.1` | Prerelease | 없음 | +| 다음 버전 정식 승격 | `main`에 병합한 `develop` | `v1.22.0` | 정식 Release | WinGet, Discord | -## 0. 사전 조건 (매 릴리스) +Retail 태그는 [`build.yml`](../.github/workflows/build.yml)을 실행하고 Preview 태그는 [`preview.yml`](../.github/workflows/preview.yml)을 실행합니다. 두 워크플로 모두 x64와 arm64 빌드, 미서명 Draft와 `PublishPayload-` 아티팩트를 생성합니다. -- **SimplySign Desktop 로그인**(서명 세션 활성) — 코드 서명 인증서가 `CurrentUser\My`에 개인 키와 함께 있어야 함. - - 확인: `Get-ChildItem Cert:\CurrentUser\My | ? { $_.Subject -like '*Jung Hyun Nam*' -and $_.HasPrivateKey }` -- ⚠️ **`signtool`이 PATH에 없을 수 있다.** 이 경우 `tools/sign-release.ps1`은 시작하자마자 실패한다. - Velopack CLI에 동봉된 것을 쓰면 된다 — `~\.dotnet\tools\.store\vpk\\vpk\\vendor\signing\signtool.exe`. +## 공통 사전 조건 - ```powershell - $signtool = (Get-ChildItem "$env:USERPROFILE\.dotnet\tools\.store\vpk" -Recurse -Filter signtool.exe | - Select-Object -First 1).FullName - ``` +릴리스 작업을 시작하기 전에 다음 상태를 확인합니다. -- ⚠️ **NativeAOT 부트스트래퍼는 C++ 빌드 도구가 있어야 만들어진다**(Visual Studio의 *Desktop - Development for C++*, arm64까지 내려면 *C++ ARM64 build tools*). 없으면 `build.cs`가 - `Platform linker not found` 경고와 함께 **조용히 건너뛰고**, `SporkBootstrap_*.exe` 4종이 로컬 - 산출물에서 빠진다. 이 경우 릴리스에는 CI가 만든 **미서명본**이 그대로 남으므로 4단계에서 별도로 - 서명해야 한다(§4-2). -- 저장소 시크릿 **`TABLECLOTH_GITHUB_PAT` = classic PAT + `public_repo` 스코프** (winget 제출용). - - 선택: `delete_repo` — 제출 실패 시 wingetcreate가 자기 포크를 정리. - - fine-grained PAT은 wingetcreate가 지원하지 않음. -- 최신 winget 수정(포크 동기화 등)이 `main`에 머지돼 있을 것 → 2단계에서 **main HEAD에 태깅**하면 자동 충족. +- 깨끗한 Git 작업 트리와 최신 원격 태그 +- 초기화한 `external/TableClothCatalog` 서브모듈 +- GitHub CLI 로그인과 저장소 Release 쓰기 권한 +- SimplySign Desktop 로그인과 활성 서명 세션 +- `CurrentUser\My`에 개인 키를 포함한 코드 서명 인증서 +- Velopack CLI가 제공하는 `signtool.exe` 또는 PATH에서 찾을 수 있는 SignTool +- x64와 arm64 GitHub Actions 러너의 가용 상태 -## 1. 버전 올리기 +SimplySign 인증서는 다음 방식으로 확인할 수 있습니다. -- [`Directory.Build.Props`](../Directory.Build.Props)의 `TableClothVersionMajor/Minor/Patch/Revision` 수정. - - ⚠️ 태그 검증(`validate-version`)은 **3-part(Major.Minor.Patch)만** 비교한다. `Revision`(4번째)은 검증하지 않으며 파일명에만 쓰인다. -- 커밋 후 `git push origin main`. - - 참고: 이 main 푸시도 build.yml을 한 번 돌리지만 **릴리스는 만들지 않는다**(릴리스 생성·버전 검증은 태그에서만 동작). - -## 2. 태그 생성·푸시 → CI가 미서명 draft 생성 - -```bash -git tag vX.Y.Z # 버전 올린 main HEAD 에 -git push origin vX.Y.Z +```powershell +Get-ChildItem Cert:\CurrentUser\My | + Where-Object HasPrivateKey | + Select-Object Subject, Thumbprint, NotAfter ``` -- build.yml: `validate-version`(태그 == props 3-part) → x64/arm64 빌드 → **미서명 draft 릴리스 생성**(`UNSIGNED` 마커, 자동 릴리스 노트, SBOM, build attestation). 완료까지 약 20–30분. -- ⚠️ **`released` 이벤트는 "태그가 가리키는 커밋"을 체크아웃**해서 winget 스크립트를 실행한다(main HEAD가 아님). 따라서 태그는 반드시 최신 `tools/winget/submit-winget.cs` + 포크 동기화 수정(commit `997cd0f` 이후)을 **포함한 커밋**이어야 한다 → **main HEAD에 태깅하면 해결**. 옛 커밋에 태깅하면 winget 단계가 깨질 수 있다. +인증서 개인 키나 PFX를 저장소 또는 CI로 내보내지 않습니다. 서명은 SimplySign 세션이 열린 로컬 PC에서만 수행합니다. -## 3. 로컬 전체 서명 빌드 +## 버전과 태그 준비 -작업 트리가 릴리스 버전(= 태그 커밋/main HEAD)인지 확인한 뒤, SimplySign 세션을 연 상태에서: +버전의 단일 출처인 [`Directory.Build.Props`](../Directory.Build.Props)에서 `Major`, `Minor`, `Patch`, `Revision`을 설정합니다. `Revision`은 `0`으로 유지하고 태그에는 세 자리 SemVer만 사용합니다. + +Retail 태그는 새 `origin/main` HEAD와 정확히 일치해야 합니다. 태그를 만들기 전에 다음 조건을 검사합니다. ```powershell -$env:TABLECLOTH_SIGN_SUBJECT = 'Jung Hyun Nam' # 필수 (또는 --sign-subject "") -.\build.cmd --sign +git fetch origin --prune --tags +git status --short --branch + +$head = (git rev-parse HEAD).Trim() +$main = (git rev-parse origin/main).Trim() +if ($head -ne $main) { throw 'Retail tag target must equal origin/main HEAD.' } ``` -- ⚠️ `--sign`에 주체가 없거나(`TABLECLOTH_SIGN_SUBJECT`/`--sign-subject`) `CurrentUser\My`에 개인 키 인증서가 없으면 **빌드 전에 즉시 실패**한다(안전장치). -- ⚠️ **연속 릴리스 주의**: Velopack 은 `Releases` 폴더에 남은 이전 버전 산출물을 보고 델타를 만들어 버전을 섞는다. 새 릴리스 전에 `Releases\`(와 `publish\`)를 비우고 빌드한다. -- 결과: `Releases\Release\x64\`, `Releases\Release\arm64\` 에 **TableCloth 와 Spork 두 앱**이 함께 — - - 서명된 `TableCloth_<4파트버전>_Release_.exe` / `Spork_<버전>_Release_.exe` (+ 각 `_Portable.zip`) - - **+ Velopack 메타데이터**(`.nupkg`, `RELEASES-*`, `releases.*.json`, `assets.*.json`) — Spork 는 채널 `spork-` 라 TableCloth(채널 ``)와 이름이 겹치지 않는다. - - 서명 범위: 앱 바이너리 + `Update.exe` + `Setup.exe` (Release 구성만). -- ⚠️ 빌드 로그 끝에서 **`(skip) bootstrapper exe not found`** 가 있는지 확인한다. 있으면 §0의 C++ - 빌드 도구가 없다는 뜻이고, `SporkBootstrap_*.exe` 는 §4-2에서 따로 서명해야 한다. 이 경고는 - 빌드를 실패시키지 않으므로(종료 코드 0) 놓치기 쉽다. +Preview 태그는 `vX.Y.Z-preview.N` 형식을 사용하며 `origin/develop` 이력에 포함된 커밋만 가리킵니다. `N`은 1 이상의 정수이고 이미 사용한 번호를 재사용하지 않습니다. -### 3-1. 서명 호스트와 아키텍처 (x64 PC 에서 arm64 를 서명해도 되는가) +```powershell +$tag = 'v1.22.0-preview.1' +if ($tag -notmatch '^v\d+\.\d+\.\d+-preview\.[1-9]\d*$') { + throw 'Invalid Preview tag format.' +} -**된다. 그리고 이미 매 릴리스 그렇게 하고 있다.** Authenticode 는 PE 파일의 바이트를 해시해 인증서 -테이블에 서명 블록을 덧붙이는 작업이고, signtool 은 대상 바이너리를 실행하지 않는다. PE 헤더의 -`Machine` 필드는 해시 대상 바이트 중 하나일 뿐이라 서명 절차와 무관하다. +git merge-base --is-ancestor HEAD origin/develop +if ($LASTEXITCODE -ne 0) { throw 'Preview tag target is not contained in origin/develop.' } +``` -실측 근거(2026-08-01) — 1.20.9 arm64 패키지 안의 앱 바이너리는 ARM64 네이티브인데 서명이 유효하고, -그 서명은 x64 개발 PC 의 `build.cmd --sign` 이 붙인 것이다. 같은 방법으로 언제든 재확인할 수 있다: +태그의 버전 코어와 `Directory.Build.Props`가 일치하는지 검토한 뒤 태그를 Push합니다. ```powershell -# 배포된 arm64 패키지에서 앱 바이너리를 꺼내 PE 아키텍처와 서명을 함께 확인 -Add-Type -AssemblyName System.IO.Compression.FileSystem -$a = [System.IO.Compression.ZipFile]::OpenRead('Releases\Release\arm64\Spork_<버전>_Release_arm64_Portable.zip') -$e = $a.Entries | Where-Object { $_.FullName -eq 'current/Spork.exe' } -[System.IO.Compression.ZipFileExtensions]::ExtractToFile($e, "$env:TEMP\check.exe", $true); $a.Dispose() -$fs = [IO.File]::OpenRead("$env:TEMP\check.exe"); $br = [IO.BinaryReader]::new($fs) -$fs.Position = 0x3C; $fs.Position = $br.ReadInt32() + 4 -'{0:X}' -f $br.ReadUInt16() # AA64 = ARM64, 8664 = x64, 14C = x86 -$br.Close(); Get-AuthenticodeSignature "$env:TEMP\check.exe" | Select-Object Status, SignerCertificate +git tag $tag +git push origin $tag ``` -- **패키징도 아키텍처 중립이다.** Velopack 의 `Setup.exe` 와 패키지 내부 `Update.exe` 는 x64/arm64 - 패키지 **양쪽 모두 x86 범용 스텁**이라(PE 헤더 확인) 패킹한 호스트의 아키텍처가 산출물에 새지 않는다. - `build.cs` 가 `vpk pack` 에 `--runtime` 을 넘기지 않고 `--channel` 로만 arch 를 구분하는 것도 이 때문에 - 문제가 되지 않는다. -- **반대 방향(arm64 PC 에서 x64 서명)** 도 원리는 같지만, SimplySign 가상 스마트카드 CSP/미들웨어가 - ARM64 Windows 에뮬레이션에서 정상 동작하는지는 **미검증**이다. 서명 호스트는 x64 하나로 고정하는 편이 - 안전하다(서명 의미론이 아니라 드라이버 호환성 문제). -- **자유롭지 않은 것은 빌드다.** Native AOT 는 대상 아키텍처 툴체인이 필요하므로 크로스로 자유로운 것은 - 서명·패키징뿐이다. 이 구분이 프리뷰 레인(AOT)에서 CI 산출물에 의존해야 하는 이유다. +현재 CI는 태그와 버전 파일을 비교하지만 브랜치 소속을 강제하지 않습니다. 위 브랜치 검사를 생략하지 않습니다. -## 4. 서명 자산 업로드 (CI 미서명본 교체) +## CI Draft와 게시 산출물 -올릴 대상은 **두 앱(TableCloth + Spork)의 설치 관리자 + Portable + Velopack 메타데이터 전체**다. -파일명이 CI와 동일한 4-part 버전이라 `--clobber`가 정확히 **교체**한다(중복 추가가 아님). -nupkg/메타데이터도 로컬 서명본으로 교체되어 설치 관리자와 일관된다. SBOM은 CI 산출물이 그대로 -유지된다(하이브리드). +태그 Push 후 해당 워크플로 실행이 성공할 때까지 기다립니다. 실패한 Job이 있으면 Draft를 게시하지 않고 원인을 수정한 새 커밋과 새 태그로 다시 진행합니다. 이미 외부에 Push한 태그를 다른 커밋으로 이동하지 않습니다. -### 4-1. 파일별 업로드 + 대조 검증 +CI가 남기는 아티팩트는 다음과 같습니다. -> ⚠️ **한 줄 glob 업로드(`gh release upload ... x64\* arm64\* --clobber`)를 쓰지 말 것.** -> v1.20.9 에서 대량 업로드가 중간에 `HTTP 404` 로 끊겼는데, `--clobber` 는 **먼저 기존 자산을 지우고** -> 올리기 때문에 **x64 설치 관리자와 nupkg 가 아예 사라진 채 남았고**, arm64 7개는 CI 미서명본이 -> 그대로 유지됐다. 명령은 부분 성공으로 끝나 조용히 넘어간다. +| 아티팩트 | 내용 | 사용처 | +| --- | --- | --- | +| `Velopack--Release` 또는 `Preview-` | 미서명 패키지와 메타데이터 | Draft의 최초 자산 | +| `PublishPayload-` | 패키징 전 Native AOT 게시 산출물 | 로컬 전체 서명 | +| `SBOM-` | SPDX SBOM | Draft와 최종 Release | -```powershell -$tag = 'vX.Y.Z' -foreach ($f in (Get-ChildItem "Releases\Release\x64\*","Releases\Release\arm64\*" -File)) { - $ok = $false - for ($i = 1; $i -le 3 -and -not $ok; $i++) { - gh release upload $tag $f.FullName --clobber 2>&1 | Out-Null - if ($LASTEXITCODE -eq 0) { $ok = $true } else { Start-Sleep -Seconds 5 } - } - '{0,-50} {1}' -f $f.Name, $(if ($ok) { 'OK' } else { 'FAILED' }) -} -``` +로컬 서명에는 `PublishPayload-x64`와 `PublishPayload-arm64`가 모두 필요합니다. 한쪽 아키텍처가 빠진 상태에서는 패키징을 시작하지 않습니다. + +## 로컬 SimplySign 전체 서명 -업로드 후 **반드시 크기 대조**로 교체 여부를 확인한다. 로컬 서명본은 CI 미서명본보다 크므로, -크기가 같으면 교체가 안 된 것이다. +저장소 루트를 확인하고 이전 패키징 산출물만 정리한 뒤 CI 아티팩트를 내려받습니다. 다음 예시는 Preview 워크플로를 사용합니다. Retail에서는 워크플로 이름을 `build.yml`로 바꿉니다. ```powershell -$assets = (gh release view $tag --json assets | ConvertFrom-Json).assets -$local = @(Get-ChildItem "Releases\Release\x64\*","Releases\Release\arm64\*" -File) | - Select-Object -ExpandProperty Name -Unique -$bad = foreach ($n in $local) { - $a = $assets | Where-Object name -eq $n - $f = Get-ChildItem "Releases\Release\*\$n" -File | Select-Object -First 1 - if (-not $a) { "MISSING: $n" } - elseif ($a.size -ne $f.Length) { "SIZE MISMATCH: $n (release=$($a.size) local=$($f.Length))" } +$repo = (git rev-parse --show-toplevel).Trim() +Set-Location $repo + +$tag = 'v1.22.0-preview.1' +$workflow = 'preview.yml' +$artifactRoot = Join-Path $env:TEMP "tablecloth-$tag-artifacts" + +Remove-Item -LiteralPath (Join-Path $repo 'publish') -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item -LiteralPath (Join-Path $repo 'Releases') -Recurse -Force -ErrorAction SilentlyContinue +Remove-Item -LiteralPath $artifactRoot -Recurse -Force -ErrorAction SilentlyContinue + +$run = (gh run list --workflow $workflow --branch $tag --limit 1 --json databaseId | + ConvertFrom-Json).databaseId +if (-not $run) { throw "No workflow run found for $tag." } + +gh run download $run --pattern 'PublishPayload-*' --dir $artifactRoot +New-Item -ItemType Directory -Path (Join-Path $repo 'publish') -Force | Out-Null +foreach ($payload in Get-ChildItem -LiteralPath $artifactRoot -Directory) { + foreach ($item in Get-ChildItem -LiteralPath $payload.FullName) { + Copy-Item -LiteralPath $item.FullName -Destination (Join-Path $repo 'publish') -Recurse -Force + } } -if ($bad) { $bad } else { "OK - 로컬 산출물 $($local.Count)개 전부 일치" } ``` -### 4-2. CI 산출 부트스트래퍼 서명 (로컬 빌드가 건너뛴 경우) - -§0/§3에서 `(skip) bootstrapper exe not found` 를 만났다면, `SporkBootstrap_*.exe` 4종은 CI 미서명본이다. -내려받아 서명하고 되올린다. +다음 네 폴더가 모두 존재하는지 확인합니다. -```powershell -$work = "$env:TEMP\tc-sign-bootstrap" -New-Item -ItemType Directory -Path $work -Force | Out-Null -Set-Location $work -gh release download $tag --repo yourtablecloth/TableCloth --pattern "SporkBootstrap*.exe" -& $signtool sign /n "Jung Hyun Nam" /tr "http://time.certum.pl" /td sha256 /fd sha256 (Get-ChildItem *.exe).FullName -Get-ChildItem *.exe | ForEach-Object { gh release upload $tag $_.FullName --repo yourtablecloth/TableCloth --clobber } +```text +publish\Release\win-x64 +publish\Release\win-arm64 +publish\spork\Release\win-x64 +publish\spork\Release\win-arm64 ``` -### 4-3. 게시 전 서명 전수 확인 +SimplySign 세션을 연 뒤 인증서 주체를 지정하여 다시 패키징합니다. Preview 번호는 태그와 같은 값을 명시합니다. ```powershell -$verify = "$env:TEMP\tc-verify-$tag" -New-Item -ItemType Directory -Path $verify -Force | Out-Null -Set-Location $verify -gh release download $tag --repo yourtablecloth/TableCloth --pattern "*.exe" -Get-ChildItem *.exe | ForEach-Object { - '{0,-48} {1}' -f $_.Name, (Get-AuthenticodeSignature $_.FullName).Status -} -``` - -**모든 항목이 `Valid` 여야 한다.** 하나라도 `NotSigned` 면 게시하지 않는다. -(v1.20.9 기준 대상은 8개 — TableCloth/Spork 설치 관리자 각 2 + SporkBootstrap 4.) - -## 5. 게시 +$env:TABLECLOTH_SIGN_SUBJECT = '' -- **§4-3의 서명 전수 확인을 통과했는지 먼저 볼 것.** -- 릴리스 노트에서 **`UNSIGNED` 마커 블록 제거**. 자동 생성 노트는 커밋 목록이라, UI 변화가 없는 - 유지 보수 릴리스일수록 **맨 앞에 사용자용 요약 문단을 덧붙이는 편**이 좋다(v1.20.9 참고). -- draft 해제(Publish) — prerelease가 아니므로 → **`released` 이벤트 발생**. +# Retail +.\build.cmd --skip-build --sign -## 6. winget 자동 제출 (자동) - -- winget_publish.yml(`released` 트리거)이 자동 실행: 포크(`rkttu/winget-pkgs`) 동기화 → `wingetcreate update --submit` → **microsoft/winget-pkgs PR** 생성. -- Actions에서 *Submit to winget-pkgs repo* 성공 + PR 생성 확인. -- **복구 경로**: 자동 실행이 실패/누락되면 Actions → *Submit to winget-pkgs repo* → **Run workflow** → `release_tag`에 `vX.Y.Z` 입력(수동 재실행). -- ⚠️ 멱등성은 "winget master에 버전 폴더 존재 여부"로 판단한다. PR이 **머지되기 전**에는 폴더가 없으므로, 재게시/재실행 시 **같은 버전의 중복 PR**이 생긴다. 첫 PR이 머지될 때까지 `released` 재발생을 피하거나 중복 PR을 닫을 것. - -## 7. 출시 후 +# Preview +.\build.cmd --skip-build --sign --preview --preview-number 1 +``` -- (선택) SNS/닷넷데브 포럼에 릴리스 소식 공유. -- winget PR이 Microsoft 측에서 검증·머지되는지 모니터링. +`--skip-build`는 CI가 생성한 Native AOT 산출물을 사용합니다. `build.cs`는 TableCloth와 Spork의 앱 바이너리, `Update.exe`, `Setup.exe`를 서명하면서 x64와 arm64 패키지를 다시 만듭니다. Authenticode는 대상 실행 파일을 실행하지 않으므로 x64 호스트에서 arm64 바이너리를 서명할 수 있습니다. ---- +Preview의 `--preview-number`가 태그와 다르면 패키지 버전도 달라집니다. 패키징 로그와 생성한 메타데이터에서 전체 SemVer를 대조합니다. -## 8. 프리뷰(Preview) 릴리스 — CI 게시 + 로컬 서명 +## Draft 자산 교체와 서명 검증 -프리뷰는 리테일 흐름(§3 의 로컬 전체 빌드)을 쓸 수 없다. **arm64 Native AOT 는 x64 개발 PC 에서 빌드할 수 -없기 때문**이다. 대신 [`preview.yml`](../.github/workflows/preview.yml) 이 x64 는 `windows-latest`, arm64 는 -**`windows-11-arm` 네이티브 러너**에서 빌드하고, **로컬은 그 산출물을 받아 다시 pack 하면서 서명만** 한다. -서명도 패키징도 아키텍처 중립이므로(§3-1) x64 PC 에서 arm64 패키지를 만들 수 있다 — 로컬에서 못 하는 것은 -arm64 **빌드**뿐이다. +로컬 결과는 `Releases\Release\x64`와 `Releases\Release\arm64`에 생성됩니다. 파일별로 업로드하고 각 명령의 성공 여부를 확인합니다. 한 번에 여러 Glob을 넘기면 일부 자산만 교체된 상태를 놓칠 수 있습니다. -```text -CI (windows-latest / windows-11-arm) 로컬 x64 PC - dotnet publish -r win- - → publish\Release\win- PublishPayload- 아티팩트 다운로드 - → publish\spork\Release\win- ──▶ → 리포 루트의 publish\ 로 전개 - vpk pack (미서명) → draft 프리릴리스 build.cmd --skip-build --sign --preview - → 서명된 자산으로 draft 자산 교체 → 게시 +```powershell +$tag = 'v1.22.0-preview.1' +$files = Get-ChildItem 'Releases\Release\x64\*','Releases\Release\arm64\*' -File + +foreach ($file in $files) { + $uploaded = $false + for ($attempt = 1; $attempt -le 3 -and -not $uploaded; $attempt++) { + gh release upload $tag $file.FullName --clobber + $uploaded = $LASTEXITCODE -eq 0 + if (-not $uploaded) { Start-Sleep -Seconds 5 } + } + if (-not $uploaded) { throw "Upload failed: $($file.Name)" } +} ``` -### 8-1. 태그 푸시 → CI +업로드 뒤에는 원격 자산의 이름과 크기를 로컬 결과와 비교합니다. -```bash -git tag v1.21.0-preview.N # -preview.N 접미사 필수 -git push origin v1.21.0-preview.N +```powershell +$remote = (gh release view $tag --json assets | ConvertFrom-Json).assets +$mismatch = foreach ($file in $files) { + $asset = $remote | Where-Object name -eq $file.Name + if (-not $asset) { "Missing: $($file.Name)" } + elseif ($asset.size -ne $file.Length) { "Size mismatch: $($file.Name)" } +} +if ($mismatch) { throw ($mismatch -join [Environment]::NewLine) } ``` -`preview.yml` 이 x64/arm64 를 빌드해 **draft 프리릴리스**(미서명)를 만들고, 아티팩트 두 종류를 남긴다. +최종 검증에서는 Draft의 모든 `.exe` 자산을 다시 내려받아 Authenticode 상태가 `Valid`인지 확인합니다. Portable ZIP 내부의 `TableCloth.exe`와 `Spork.exe`도 풀어서 같은 검사를 수행합니다. x64와 arm64 자산, Velopack 채널 메타데이터와 SBOM이 모두 있는지도 함께 확인합니다. -| 아티팩트 | 내용 | 용도 | -| --- | --- | --- | -| `Preview-` | `releases/*` (미서명 패키지) | draft 릴리스 자산 + 폴백 | -| `PublishPayload-` | `publish/**` (pack 이전 게시 산출물, pdb 제외) | **로컬 서명 인계용** | +서명 검증이나 자산 대조가 하나라도 실패하면 Draft를 유지합니다. CI 미서명 자산을 남겨 둔 채 일부 파일만 게시하지 않습니다. + +## 릴리스 노트와 게시 -### 8-2. 게시 산출물 내려받아 전개 +릴리스 노트 첫 부분에서 `UNSIGNED` 경고 블록을 제거하고 사용자 관점의 변경 요약을 추가합니다. Preview에는 불안정 가능성과 Retail 복귀 절차를 남깁니다. 정식 버전에는 주요 변경, 호환성 영향과 알려진 문제를 기록합니다. -리포 루트에서 (SimplySign 세션은 아직 필요 없다): +Preview는 Prerelease 상태를 유지하여 게시합니다. ```powershell -# 이전 잔여물 제거 — Velopack 이 남은 산출물로 델타를 만들어 버전을 섞는다(§3 주의와 동일). -Remove-Item publish, Releases -Recurse -Force -ErrorAction SilentlyContinue - -$run = (gh run list --workflow preview.yml --limit 1 --json databaseId | ConvertFrom-Json).databaseId -gh run download $run --pattern 'PublishPayload-*' --dir .artifacts - -# 아티팩트 루트는 publish\ 의 *내용물*이다(actions/upload-artifact 가 매칭된 파일들의 공통 조상을 -# 루트로 잡으므로 publish\ 접두사가 빠진다 — v1.21.0-preview.2 에서 실측). 즉 아티팩트 안은 -# Release\win-\... 와 spork\Release\win-\... 이므로 publish\ 아래로 부어 넣는다. -New-Item -ItemType Directory publish -Force | Out-Null -foreach ($a in (Get-ChildItem .artifacts -Directory)) { - foreach ($child in (Get-ChildItem $a.FullName)) { Copy-Item $child.FullName publish -Recurse -Force } -} -Get-ChildItem publish -Directory -Recurse | Where-Object Name -like 'win-*' | - Select-Object -ExpandProperty FullName +gh release edit v1.22.0-preview.1 --draft=false --prerelease ``` -기대되는 네 폴더가 모두 있어야 한다. **한쪽 arch 만 있으면 실패한다** — `build.cs` 는 x64 → arm64 순서로 -돌면서 각 arch 의 TableCloth·Spork 를 모두 pack 하므로, x64 가 없으면 거기서 중단되고 arm64 는 시도조차 -하지 않는다(실측 확인). +Retail은 Prerelease가 아닌 정식 Release로 게시하고 최신 정식 버전으로 지정합니다. -```text -publish\Release\win-x64 publish\spork\Release\win-x64 -publish\Release\win-arm64 publish\spork\Release\win-arm64 +```powershell +gh release edit v1.21.1 --draft=false --latest ``` -### 8-3. 로컬 서명 패키징 +게시 명령은 외부 사용자에게 자산을 노출합니다. 사용자가 릴리스 게시를 요청한 범위에서만 실행합니다. Draft 생성이나 서명까지만 요청한 경우에는 검증 결과와 남은 단계를 보고하고 Draft를 유지합니다. -SimplySign 세션을 연 상태에서: - -```powershell -$env:TABLECLOTH_SIGN_SUBJECT = 'Jung Hyun Nam' -.\build.cmd --skip-build --sign --preview --preview-number N # N = 태그의 -preview.N 과 동일 -``` +## 정식 게시 후 자동화 -- `--skip-build` 가 빌드·게시를 건너뛰고 위 `publish\` 폴더만으로 pack 한다(실측: 빌드 로그 없이 곧장 - pack, 4개 패키지 ~35초). -- `--preview` 는 채널(`preview-` / `spork-preview-`)·자산명(`-Preview`)·버전(`X.Y.Z-preview.N`)을 - 프리뷰 규칙으로 맞추고, 무설치 고정 URL 별칭은 만들지 않는다(Retail 전용 계약). -- ⚠️ `--preview-number` 가 태그와 어긋나면 **조용히 다른 버전**이 나온다. 릴리스 자산명과 대조할 것. -- 결과는 §3 과 같은 자리(`Releases\Release\\`)에 나오며, 서명 범위도 리테일과 같다 - (앱 바이너리 + `Update.exe` + `Setup.exe`). +Retail 정식 게시에서는 `release: released` 이벤트가 다음 워크플로를 시작합니다. -### 8-4. 업로드 · 게시 +- [`winget_publish.yml`](../.github/workflows/winget_publish.yml): `microsoft/winget-pkgs` 업데이트 Pull Request 생성 +- [`discord_release.yml`](../.github/workflows/discord_release.yml): Discord 출시 공지 게시 -§4-1 의 파일별 업로드 + 크기 대조 절차를 그대로 쓴다(태그만 프리뷰 태그로). +두 워크플로의 실행 성공과 실제 외부 결과를 각각 확인합니다. 워크플로가 시작됐다는 사실만으로 WinGet Pull Request나 Discord 메시지가 생성됐다고 판단하지 않습니다. -⚠️ **게시 전에 릴리스 노트의 `UNSIGNED — DO NOT PUBLISH` 블록을 제거한다.** 리테일(§5)과 같은 절차인데 -프리뷰 절에는 빠져 있어 v1.21.0-preview.2 를 그 블록이 남은 채 게시했고, 사후에 고쳐야 했다(자산은 -서명돼 있었고 노트만 틀렸다). 프리뷰 노트는 `> [!NOTE]` 로 시작하는 미리 보기 배너부터 시작해야 한다. +Preview 게시에서는 두 워크플로가 실행되지 않아야 합니다. Preview를 `/releases/latest`로 지정하지 않고 WinGet에 수동 제출하지 않습니다. -```powershell -$tag = 'v1.21.0-preview.N' -$body = (gh release view $tag --json body | ConvertFrom-Json).body -$idx = $body.IndexOf('> [!NOTE]') # 미리 보기 배너 앞(=UNSIGNED 블록)을 잘라낸다 -$tmp = Join-Path $env:TEMP 'notes.md' -[IO.File]::WriteAllText($tmp, $body.Substring($idx), (New-Object Text.UTF8Encoding $false)) -gh release edit $tag --notes-file $tmp -``` +## 출시 후 검증과 브랜치 정리 -게시는 **prerelease 를 유지**해야 한다. +릴리스 게시 후 다음 결과를 기록합니다. -```powershell -gh release edit v1.21.0-preview.N --draft=false --prerelease -``` +1. GitHub Release의 Draft와 Prerelease 상태를 확인합니다. +2. 태그 커밋과 대상 브랜치의 관계를 다시 확인합니다. +3. x64와 arm64 설치 관리자 및 Portable 자산을 확인합니다. +4. 서명 상태와 Velopack 채널 메타데이터를 확인합니다. +5. Retail에서는 WinGet Pull Request와 Discord 공지를 확인합니다. +6. 핫픽스에서는 수정 커밋을 `develop`으로 순방향 전파합니다. +7. 병합과 전파를 확인한 뒤 임시 브랜치 정리 여부를 결정합니다. -`prereleased` 이벤트만 발생하므로 winget·discord 는 발동하지 않는다. `--latest` 를 붙이지 말 것. +원격 브랜치를 삭제하기 전에 병합 커밋과 필요한 태그가 원격에 존재하는지 확인합니다. 이전 Minor 버전의 유지보수 계획이 남아 있다면 해당 브랜치를 삭제하지 않습니다. -> **폴백(설치 관리자 외피만 서명):** 위 경로가 막히면 draft 의 `Setup.exe` 4개만 `sign-release.ps1` 로 -> 서명할 수 있다. v1.21.0-preview.1 이 이 방식이었고, 이 경우 **nupkg 내부 앱 바이너리와 `Portable.zip` -> 내용물은 미서명으로 남는다**(nupkg 는 Velopack RELEASES 해시에 묶여 있어 사후 재서명 불가). 어디까지나 -> 폴백이며, 정상 경로는 8-2/8-3 이다. +## 실패 복구 경로 ---- +CI 빌드가 실패하면 같은 태그를 이동하지 않고 수정 커밋 뒤에 새 Preview 번호나 새 Patch 버전을 사용합니다. 아직 Push하지 않은 로컬 태그는 원인을 수정한 뒤 다시 만들 수 있습니다. -## 대안 / 참고 +자산 업로드가 부분 실패하면 Draft를 유지하고 로컬 파일 목록과 원격 자산을 다시 대조합니다. `--clobber`가 기존 자산을 먼저 제거할 수 있으므로 실패한 파일만 다시 올린 뒤 전체 목록을 검증합니다. -- **외부 설치 관리자만 서명**: [`tools/sign-release.ps1`](../tools/sign-release.ps1) `-Tag vX.Y.Z` → 릴리스의 **모든 `.exe`(x64+arm64 Setup)**를 내려받아 signtool로 서명·재업로드한다. 단 패키지 내부 앱 바이너리/`Update.exe`와 `Portable.zip`은 서명되지 않는다(설치 관리자 외피만 서명). 프리뷰 레인이 현재 이 방식을 쓴다(§8). -- 릴리스 바이너리는 로컬 서명 빌드 산출물이고 SBOM/노트는 CI 빌드 기준인 **하이브리드** 구조다. 장기적으로 CI 클라우드 서명으로 전환할 수 있다(자동 메모 `code_signing_approach` 참고: Azure Artifact Signing은 한국 개인 가입 제약, SSL.com eSigner는 유료 무인 옵션). +WinGet 자동 제출이 실패하면 Actions의 `Submit to winget-pkgs repo` 워크플로를 `release_tag` 입력과 함께 수동 실행할 수 있습니다. 기존 Pull Request가 아직 병합되지 않았다면 중복 제출 여부를 먼저 확인합니다. diff --git a/src/TableCloth.App/App.axaml.cs b/src/TableCloth.App/App.axaml.cs index 9c0704a2..79217c83 100644 --- a/src/TableCloth.App/App.axaml.cs +++ b/src/TableCloth.App/App.axaml.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; @@ -52,7 +53,9 @@ public override void OnFrameworkInitializationCompleted() // 이슈 #296: WPF 시절 Program.cs 의 호스트-전 라이선스 게이트를 App 라이프사이클로 이관. if (!Bootstrap.LicenseGate.EnsureAgreed()) { - desktop.Shutdown(1); + // OnFrameworkInitializationCompleted 안에서 즉시 Shutdown하면 Dispatcher가 MainLoop 시작 전에 + // 종료되어 StartCore가 InvalidOperationException을 던진다. 초기화가 반환된 뒤 종료하도록 예약한다. + Dispatcher.UIThread.Post(() => desktop.Shutdown(1)); base.OnFrameworkInitializationCompleted(); return; } diff --git a/src/TableCloth.App/Bootstrap/LicenseGate.cs b/src/TableCloth.App/Bootstrap/LicenseGate.cs index 17989383..8120f8f6 100644 --- a/src/TableCloth.App/Bootstrap/LicenseGate.cs +++ b/src/TableCloth.App/Bootstrap/LicenseGate.cs @@ -29,7 +29,8 @@ public static bool EnsureAgreed() return true; var window = new LicenseWindow(); - var agreed = DialogHost.ShowModal(window, null) == true && window.LicenseAccepted; + var dialogResult = DialogHost.ShowModal(window, null); + var agreed = IsAgreementAccepted(dialogResult, window.LicenseAccepted); if (agreed) { @@ -47,6 +48,14 @@ public static bool EnsureAgreed() return false; } + /// + /// 첫 실행에는 소유자 창이 없어 이 일반 창으로 라이선스 창을 표시한다. + /// 이 경로에서는 Avalonia의 Close(true) 결과를 회수할 수 없으므로 창이 기록한 명시적 동의 상태도 + /// 함께 사용한다. + /// + internal static bool IsAgreementAccepted(bool? dialogResult, bool licenseAccepted) + => dialogResult == true || licenseAccepted; + private static bool IsLicenseAgreed() { try diff --git a/src/TableCloth.Test/LicenseGateTests.cs b/src/TableCloth.Test/LicenseGateTests.cs new file mode 100644 index 00000000..633ededc --- /dev/null +++ b/src/TableCloth.Test/LicenseGateTests.cs @@ -0,0 +1,30 @@ +using TableCloth.Bootstrap; + +namespace TableCloth.Test; + +[TestClass] +public sealed class LicenseGateTests +{ + [TestMethod] + public void IsAgreementAccepted_OwnerlessDialogWithAcceptedState_ReturnsTrue() + { + var result = LicenseGate.IsAgreementAccepted(dialogResult: null, licenseAccepted: true); + + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(null, false, false)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(false, true, true)] + public void IsAgreementAccepted_CombinesDialogResultAndWindowState( + bool? dialogResult, + bool licenseAccepted, + bool expected) + { + var result = LicenseGate.IsAgreementAccepted(dialogResult, licenseAccepted); + + Assert.AreEqual(expected, result); + } +}