수정 이력(7건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대한 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하십시오.
- 「완성형 워크플로」가 서명 키를 승인자가 있는 Environment로 지키고 있지 않았습니다. 5.5절에서 「서명용 시크릿은 승인자가 있는 Environment에 둔다」고 적어 두었는데, 그대로 두면 동작한다고 안내하는 본문은 리포지토리 시크릿을 직접 참조하고 있어, 쓰기 권한이 있는 사람은 워크플로를 고친 뒤 태그를 하나만 push하면 키를 꺼낼 수 있습니다. job에 `environment: release-signing`을 붙였습니다. 아울러 서명 대상을 `publish` 아래의 모든 EXE/DLL로 두었던 것을 자사 빌드만으로 좁혔습니다. 의존 패키지까지 서명하면, 벤더 서명을 자사 인증서로 바꾸고(`/as` 없는 sign은 기존 서명을 치환합니다), 제3자의 미서명 DLL을 자사 산출물로 배포하게 됩니다.
- 서명 워크플로가 실행 파일 하나만 서명하고, 함께 넣는 DLL은 미서명인 채로 배포물에 들어가는 쓰임새였으므로, 배포물에 들어가는 실행 파일과 DLL을 한꺼번에 서명하는 형태로 고쳤습니다.
- 클라우드 서명 서비스를 일본 법인에서는 이용할 수 없다는 점을 독립된 절로 두고, 상황별 대안을 표로 정리했습니다. 아울러 대상 독자와 전제, PFX 방식의 완성형 워크플로, GitHub 릴리스 첨부 수단, 파이프라인 전체 그림을 추가했습니다.
- 본문의 관련 글 링크 문구가 링크 대상의 현재 제목과 어긋나 있던 것을, 실제 제목에 맞췄습니다. 본문 내용은 바꾸지 않았습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22174324)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「WinForms / WPF 앱의 CI/CD 실전 ── GitHub Actions로 빌드부터 서명·배포까지 자동화하기」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/winforms-wpf-cicd-github-actions/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22174324
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22174325
「릴리스는 그 사람 PC가 아니면 빌드를 못 해요」── WinForms나 WPF 업무 앱 상담에서 정말 자주 듣는 말입니다. 자기 PC의 Visual Studio에서 Release 빌드를 하고, zip으로 묶어 공유 폴더에 둡니다. 동작은 하지만, 그 개발자가 쉬는 날에 버그 수정판을 낼 수 있는지 아무도 답하지 못합니다.
웹 앱 CI/CD는 정보가 넘치는데, 데스크톱 앱이 되면 갑자기 빈약해집니다. 「배포 대상이 서버가 아니라 고객사 PC」라는 근본 차이가 있으니, 웹 글을 그대로 따라 할 수 없는 것도 사실입니다. 다만 빌드와 테스트 자동화까지라면, 데스크톱 앱도 웹과 거의 같은 수고로 구성할 수 있습니다. 장벽이 되는 것은 그다음 서명과 배포이며, 그 부분은 배포 방식마다 현실적인 답이 다릅니다.
이 블로그에서는 「Windows 앱 배포 방식 판단표」에서 배포 방식을 고르는 법을, 「SmartScreen과 코드 서명」에서 서명을 바라보는 관점을 정리했습니다. 이 글은 그 두 편을 전제로, GitHub Actions로 WinForms / WPF 앱의 빌드·테스트·버전 부여·서명·배포물 작성을 어디까지 자동화할지를 실무 관점에서 정리합니다.
대상 독자와 전제
자기 상황에 대입해 읽을 수 있도록, 이 글이 전제로 두는 조건을 먼저 적습니다.
- 대상: WinForms / WPF로 작성한 Windows 데스크톱 앱을, 자기 PC의 Visual Studio에서 빌드해 배포하고 있는 개발자·팀. CI/CD 경험은 묻지 않습니다.
- 리포지토리: 소스가 GitHub 리포지토리에 들어 있을 것(공개·비공개는 가리지 않습니다). 퍼블릭 리포지토리에서는 표준 러너가 무료이고, 프라이빗 리포지토리에서는 실행 시간이 분 단위로 과금됩니다.1
- 프로젝트 형식: 본문 YAML은 .NET SDK 형식 csproj(
net8.0-windows등)를 전제로 합니다. .NET Framework 4.x의 구형식 csproj여도 사고방식은 같고,dotnet build대신 MSBuild와 NuGet CLI를 사용합니다(3장). - 서명: 5장은 「앞으로 인증서를 어떻게 마련할지」부터 다룹니다. 이미 PFX 파일이나 사내 CA가 있는지, 이제 공인 인증서를 받을지에 따라 결론이 달라지므로, 현재 상태를 확인하며 읽어 주십시오.
- 목표: 전면 자동 배포가 아니라, 우선 「누구의 PC에서든 빌드·테스트가 재현되고, 산출물을 꺼낼 수 있는」 상태(3장)입니다. 거기서부터 서명·배포를 단계적으로 더합니다.
파이프라인 전체 모양은 다음과 같습니다. 자동화 범위를 어디까지로 둘지는 2장에서 다룹니다.
[일상] main 으로의 push / 풀 리퀘스트
└→ checkout → setup-dotnet → build → test → publish → upload-artifact (3장)
[릴리스] v1.2.3 태그의 push
└→ checkout → setup-dotnet → test
→ 태그에서 버전을 주입해 publish (4장)
→ 서명(signtool / 클라우드 서명 서비스)(5장)
→ 배포물을 만든다(zip / MSI / MSIX / ClickOnce)(6장)
→ GitHub 릴리스에 첨부(장기 보관)(4장)
1. 먼저 결론
- 가장 큰 위험은 「개발자 PC에서만 빌드된다」는 상태입니다. CI/CD의 첫 목표는 배포 전면 자동화가 아니라, 누구의 PC에도 묶이지 않고 빌드가 재현되는 것입니다.
- 최소 구성은 빌드+테스트 자동화만으로도 충분히 가치가 있습니다.
windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact를 YAML 한 파일로 구성할 수 있습니다.12 - WinForms / WPF는 Windows 러너가 전제입니다.
net8.0-windows처럼 Windows 전용 TFM을 대상으로 하므로3, 테스트 실행까지 CI에서 하려면 Windows 환경이 필요합니다. - 버전 부여는 태그 기반이 실무의 타협점입니다.
v1.2.3태그 push로 릴리스 빌드를 시작하고, MSBuild의Version속성에 태그 값을 주입합니다.4 - 서명이 자동화의 가장 큰 장벽입니다. 2023년 6월 이후, 공인 OV 인증서의 비밀 키는 HSM 보관이 필수가 되어 「PFX를 시크릿에 두고 signtool」이라는 예전 정석을 그대로는 쓸 수 없습니다. CI 연동이 쉬운 Azure Artifact Signing(구 Trusted Signing)은 일본이 대상 지역에 들어 있지 않기 때문에, 일본 개발자의 현실적인 답은 CA의 클라우드 HSM 옵션이거나, 서명 단계만 로컬에 남기는 구성입니다(5.2절).5
- 배포 형식에 따라 CI에 올리기 쉬운 정도가 크게 다릅니다. xcopy(zip)가 가장 쉽고, MSIX는 서명이 필수6, MSI는 WiX 등의 CLI 연동, ClickOnce는
msbuild /target:publish가 필요해 까다롭다는 순서입니다.7 - UI 자동 테스트를 CI의 필수 관문으로 두지 마십시오. 유닛 테스트는 CI 필수, UI 테스트는 스모크로 좁혀 별도 job으로 돌리는 편이 현실적입니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 21건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 데스크톱 앱 CI/CD는 웹과 무엇이 다른가
웹 앱 CI/CD의 틀(push→빌드→테스트→서버 배포)을 그대로 가져올 수 없는 이유를 먼저 정리합니다.
| 관점 | 웹 앱 | WinForms / WPF 데스크톱 앱 |
|---|---|---|
| 배포 대상 | 우리가 관리하는 서버 | 고객사·현장 PC(관리 밖) |
| 배포 단위 | 서버에서 일괄 전환 | MSI / MSIX / ClickOnce / zip 등 다양. 배포 시점은 상대에게 달림 |
| 롤백 | 서버 쪽에서 되돌릴 수 있음 | 이미 배포된 PC에서는 쉽게 되돌리기 어렵다. 구버전 인스톨러 보관이 필수 |
| 서명 | 보통 불필요(TLS는 인프라 쪽) | 실행 파일·패키지 코드 서명이 사실상 필수 |
| 빌드 환경 | Linux 러너로 끝내기 쉬움 | Windows 러너가 전제 |
| 테스트 | 헤드리스로 끝내기 쉬움 | 유닛 테스트는 같다. UI 테스트는 데스크톱 세션이 필요 |
| 「배포」의 의미 | 프로덕션 반영까지 | CI 범위는 「배포물 완성」까지. 설치는 별도 공정 |
중요한 것은 마지막 행입니다. 데스크톱 앱에서 CI/CD 파이프라인의 출구는 「프로덕션 반영」이 아니라 「서명이 끝난 배포물이, 언제든 꺼낼 수 있는 장소에 놓여 있는 것」입니다. 그다음(고객사 배포, 자동 업데이트)은 배포 방식 설계의 이야기이며, 「배포 방식 판단표」에서 다룬 영역입니다. 바꿔 말하면, 출구를 그렇게 잘라 두면 데스크톱 앱 CI/CD도 웹과 같은 도구로 구성할 수 있습니다.
3. 최소 구성 ── GitHub Actions로 빌드+테스트
처음에 넣어야 할 것은 이것뿐입니다. GitHub 호스팅 러너는 job마다 새 VM이 할당되므로1, push할 때마다 깨끗한 Windows에서 빌드와 테스트가 돌아가고, 「그 사람 PC에만 들어 있는 SDK」에 대한 의존이 그 자리에서 드러납니다.
name: build-and-test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: windows-latest # WinForms / WPF 는 Windows 러너 필수
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Test
run: dotnet test --configuration Release --no-build
- name: Publish
run: dotnet publish src/MyApp/MyApp.csproj -c Release -o publish
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: MyApp
path: publish
이 YAML을 .github/workflows/build-and-test.yml로 두면, 그대로 push하면 동작합니다. 다만 src/MyApp/MyApp.csproj 부분은 자기 리포지토리의 프로젝트 경로로 바꿔 주십시오. 이후 YAML에서도 같은 경로를 예로 씁니다. 리포지토리 바로 아래에 솔루션과 csproj 하나만 있는 구성이라면, dotnet publish -c Release -o publish처럼 경로를 생략해도 동작합니다.
포인트를 세 가지 보탭니다.
첫째, runs-on: windows-latest가 기본입니다. WinForms / WPF 프로젝트는 TargetFramework가 net8.0-windows처럼 Windows 전용 TFM이고, UseWindowsForms 또는 UseWPF를 켠 .NET 데스크톱 SDK 프로젝트입니다.3 엄밀히 말해 컴파일만이면 Linux 러너에서도 EnableWindowsTargeting을 켜면 빌드할 수 있지만, dotnet test처럼 실행이 따르는 단계에는 Windows 환경이 필요하므로, 테스트까지 한 job으로 돌리는 이 구성에서는 그냥 Windows 러너를 씁니다. .NET Framework 4.x(구형식 csproj)에서는 dotnet build가 아니라 MSBuild와 NuGet CLI를 쓰지만, 둘 다 Windows 러너에 미리 설치되어 있고 사고방식은 같습니다.
둘째, actions/upload-artifact로 산출물을 반드시 남깁니다. 「그 빌드의 산출물 일체를 GitHub에서 꺼낼 수 있다」는 것이, 특정인 PC 의존에서 벗어난 실체입니다. 급한 동작 확인본도 Actions 화면에서 zip만 받으면 됩니다.
셋째, 이 단계에서는 서명도 배포도 아직 하지 않습니다. 이 최소 구성만으로 「main은 언제나 빌드·테스트 가능」, 「누구나 같은 산출물을 꺼낼 수 있다」는 두 가지 보증이 생기고, 필자 경험상 소규모 팀 고민의 대부분은 여기서 해소됩니다.
4. 버전 번호 자동 부여 ── 태그 기반 릴리스
다음 단계는 「이 zip, 버전이 몇인가?」 문제를 없애는 일입니다. 로컬 빌드 운용에서는 csproj의 Version을 고치는 것을 잊고 같은 1.0.0이 여러 세대 남는 사고가 정석입니다. 실무의 타협점은 태그 기반 릴리스입니다. 릴리스할 커밋에 v1.2.3 같은 태그를 치면 그것을 트리거로 워크플로가 돌고, 태그 이름에서 꺼낸 버전을 빌드에 주입합니다.
name: release
on:
push:
tags: [ 'v*' ]
jobs:
release:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# 태그 push에서는 3장의 빌드+테스트 워크플로가 실행되지 않으므로,
# 릴리스 산출물을 만들기 전에 여기서도 테스트를 통과시킨다
- name: Test
run: dotnet test --configuration Release
- name: Publish with version from tag
shell: pwsh
run: |
$version = $env:GITHUB_REF_NAME.TrimStart('v') # v1.2.3 -> 1.2.3
dotnet publish src/MyApp/MyApp.csproj `
-c Release -o publish `
-p:Version=$version
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: MyApp-${{ github.ref_name }}
path: publish
-p:Version=1.2.3처럼 MSBuild 속성으로 넘기면, .NET SDK 프로젝트에서는 AssemblyVersion과 FileVersion이 Version의 접두사(접미사를 뺀 부분)에서, InformationalVersion이 Version 자체에서 기본 생성됩니다.4 csproj에는 개발용 임시 값만 두고, 릴리스 시점의 정식 버전은 태그만 갖도록 한곳에서 관리합니다.
주의가 하나 있습니다. actions/upload-artifact 산출물에는 리포지토리 보관 기간(기본 90일)이 있고, 만료되면 사라집니다. 데스크톱 앱은 롤백용으로 구버전 인스톨러를 오래 보관해야 하므로, 태그 빌드 산출물은 GitHub 릴리스에 첨부하는 식으로 항구적인 장소에 발행하고, Actions 아티팩트는 임시 전달로 잘라 생각합니다.
GitHub 릴리스 첨부는 전용 액션을 쓰는 방법과, 러너에 처음부터 들어 있는 GitHub CLI(gh)를 쓰는 방법이 있습니다.8 어느 쪽이든 job에 contents: write 권한이 필요합니다.
jobs:
release:
runs-on: windows-latest
permissions:
contents: write # 릴리스 생성·에셋 첨부에 필요
steps:
# ...(빌드와 publish는 앞에서와 같음)
- name: Zip
shell: pwsh
run: Compress-Archive -Path publish\* -DestinationPath MyApp-${{ github.ref_name }}.zip
# 방법 A: 전용 액션을 사용
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: MyApp-${{ github.ref_name }}.zip
# 방법 B: 러너에 포함된 GitHub CLI를 사용(위 둘 중 하나만 하면 됨)
- name: Create GitHub Release (gh)
shell: pwsh
run: gh release create ${{ github.ref_name }} MyApp-${{ github.ref_name }}.zip --generate-notes
env:
GH_TOKEN: ${{ github.token }}
gh는 GitHub 호스팅 러너에 미리 설치되어 있지만, 스텝마다 GH_TOKEN 환경 변수에 필요한 스코프를 가진 토큰을 넘겨야 합니다.8
이점은 운용이 Git 안에 닫힌다는 점입니다. 「고객 환경의 1.2.3은 어느 커밋인가」는 태그로 확정되고, EXE 속성에 나오는 파일 버전과 Git 태그가 기계적으로 일치합니다. 게다가 InformationalVersion에는 .NET 8 SDK 이후 Git 커밋 해시(SourceRevisionId)가 기본으로 붙으므로4, 버전 표시 화면에 이를 보여 두면 산출물에서 커밋을 바로 특정할 수 있습니다.
5. 코드 서명을 CI에 넣기 ── 여기가 가장 큰 장벽
빌드와 버전까지는 순조롭게 자동화한 팀이 거의 반드시 멈추는 곳이 서명입니다. Store 밖에서 배포하는 Windows 앱에 코드 서명이 사실상 필수인 이유(SmartScreen, 기업의 보안 제품, 변조 탐지)는 「SmartScreen과 코드 서명」에서 정리했으므로, 여기서는 CI의 어디에서, 어떻게 실행할지에만 초점을 둡니다.
5.1 signtool의 기본형
서명 실행 자체는 명령 하나입니다. signtool은 Windows SDK에 포함되어 GitHub Windows 러너에서도 쓸 수 있습니다. 현재 SDK에서는 /fd(파일 다이제스트)와 /td(타임스탬프 다이제스트) 지정이 필수이고, SHA256이 권장입니다.9
signtool sign /f MyCert.pfx /p $env:PFX_PASSWORD `
/fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
publish\MyApp.exe
타임스탬프(/tr)는 생략할 수 있지만, 반드시 붙입니다. 타임스탬프가 있으면 인증서 만료 후에도 「서명 시점에는 유효했다」는 것을 검증할 수 있어, 이미 배포된 파일의 서명이 계속 살아 있습니다.96
5.2 인증서 종류와 CI에 올리는 현실
문제는 명령이 아니라 비밀 키를 어디에 둘지입니다. 인증서를 어떻게 받느냐에 따라 CI에 올리는 방법이 근본적으로 달라집니다.
| 인증서 형태 | 비밀 키 위치 | CI에 올리기 | 비고 |
|---|---|---|---|
| 클라우드 서명 서비스(Azure Artifact Signing = 구 Trusted Signing 등) | 클라우드 쪽 | 올리기 쉽다. GitHub Actions 등과의 연동이 전제인 설계 | 이용 가능한 국가·지역 제한이 있고, 일본은 대상 외(법인은 미국·캐나다·EU·영국, 개인은 미국·캐나다만). 자세한 내용은 아래5 |
| OV 인증서(2023년 6월 이후 신규 발행) | HSM / USB 토큰 필수 | 토큰을 러너에 꽂을 수 없어 그대로는 불가. CA의 클라우드 HSM 옵션이면 가능 | CA/Browser Forum 요건에 따름5 |
| EV 인증서 | HSM / USB 토큰 | 동일 | SmartScreen 즉시 신뢰 효과는 2024년에 폐지됨. 서명 운용에서는 OV와 같은 줄로 본다5 |
| 예전 PFX 파일(과거에 발행된 것·사내 CA·자체 서명) | 파일 | 시크릿에 Base64로 저장해 복원(아래) | 공개 배포용으로 새로 받는 형태는 원칙적으로 더 이상 구할 수 없음 |
즉, 검색에 자주 나오는 「PFX를 GitHub 시크릿에 두고 signtool로 서명」 구성은 사내 CA나 기존 PFX에서는 지금도 유효하지만, 이제 공인 인증서를 받는 경우에는 전제가 무너져 있습니다. 새로 짤 때는 CI 연동을 처음부터 지원하는 클라우드 서명 서비스를 축으로 검토하는 편이 현실적입니다.5 USB 토큰으로 운용 중이라면, 서명 단계만 로컬 PC나 토큰을 꽂은 셀프 호스팅 러너에 남기는 절충 구성이 됩니다.
일본 개발자에게 본론 ── Azure Artifact Signing을 쓸 수 있는가
표 첫 행에 「이용 가능한 국가·지역 제한이 있다」고 썼지만, 일본 독자에게는 여기가 가장 중요한 판단 재료이므로 따로 정리합니다.
Microsoft 문서는 Azure Artifact Signing(구 Trusted Signing)을 쓸 수 있는 것은, 법인은 미국·캐나다·EU·영국, 개인 개발자는 미국·캐나다에 한정된다고 명시합니다.5 즉 일본 법인·일본 거주 개인 개발자는 현재 대상 외입니다. 「클라우드 서명이 정석이다」라는 일반론을 그대로 가져오면, 계정 생성 단계에서 멈춥니다. 이 제한을 전제로 한 선택지는 다음과 같습니다.
| 상황 | 현실적인 선택 |
|---|---|
| 이제 공인 인증서를 받는다(일본 법인) | OV 인증서+CA의 클라우드 HSM 옵션. 2023년 6월 이후 OV 인증서의 비밀 키는 HSM 또는 하드웨어 토큰 보관이 필수지만, 많은 CA가 USB 토큰 외에 클라우드 HSM 선택지도 마련해 두며, 그쪽이면 CI에서 서명을 호출할 수 있습니다.5 CI에 올릴 계획이 있다면, 인증서를 고르는 단계에서 클라우드 HSM 대응 여부를 CA에 확인하십시오. 토큰을 산 뒤에는 바꿀 수 없습니다 |
| 이미 USB 토큰으로 운용 중 | 서명 단계만, 토큰을 꽂은 로컬 PC 또는 셀프 호스팅 러너에 남깁니다. 빌드·테스트·버전 부여까지는 GitHub 호스팅 러너로 자동화하고, 마지막 서명만 사람의 손을 남기는 구성입니다 |
| Microsoft Store(MSIX)로 배포할 수 있다 | Store 쪽에서 Microsoft가 다시 서명하므로, 자체 인증서가 필요 없어집니다.5 배포 방식을 다시 고를 수 있다면, 서명 고민이 같이 사라지는 최단 경로입니다(다만 MSI/EXE 인스톨러로 Store에 내는 경우에는 publisher 쪽 서명이 필요합니다) |
| 오픈 소스 프로젝트 | SignPath Foundation이, 조건을 충족하는 OSS 프로젝트에 무상 코드 서명을 제공합니다.5 |
| 사내 배포만 | 사내 CA에서 발급한 인증서와 기존 PFX 방식으로 충분합니다(5.3절). 인증서를 그룹 정책이나 Intune으로 신뢰된 루트에 배포할 수 있는 환경이라면, 공인 인증서는 필요 없습니다 |
Azure Artifact Signing의 대상 지역은 앞으로 넓어질 수 있으므로, CI/CD 서명 설계를 굳힐 때는 그 시점의 대상 지역을 1차 정보로 확인하십시오.5
5.3 시크릿 관리 주의점
PFX 방식(사내 CA·기존 인증서)을 CI에 올릴 때의 정석입니다.
- PFX는 Base64 문자열로 GitHub 시크릿에 저장하고, job 안에서 파일로 복원합니다. 바이너리를 시크릿으로 다루는 방법으로 GitHub Docs가 안내하는 절차입니다.10
- 비밀번호는 별도 시크릿으로 둡니다. 시크릿 값은 로그에서 자동으로 마스크되지만10, 가공한 파생 값까지는 지켜지지 않습니다. 서명 스텝 외에 환경 변수를 넘기지 마십시오.
- 포크에서 온 풀 리퀘스트에는(
GITHUB_TOKEN을 제외하고) 시크릿이 넘어가지 않습니다.10 다만 job 자체는 빈 시크릿으로 실행되므로, 위의 복원 스텝은 빈 문자열의 Base64 디코드에서 실패합니다. 서명 스텝은 4장처럼 태그로 시작하는 릴리스 워크플로(포크 PR에서는 실행되지 않음)로 분리하거나,if: github.event_name != 'pull_request'같은 조건으로 명시적으로 건너뛰게 합니다.
- name: Restore signing certificate
shell: pwsh
run: |
$bytes = [Convert]::FromBase64String($env:PFX_BASE64)
[IO.File]::WriteAllBytes("$env:RUNNER_TEMP\sign.pfx", $bytes)
env:
PFX_BASE64: ${{ secrets.SIGNING_PFX_BASE64 }}
5.4 PFX 방식의 완성형 워크플로
여기까지의 조각(태그 기반 버전 주입·PFX 복원·signtool 실행·산출물 발행)을 한 줄로 이은 형태를 보입니다. 사내 CA나 기존 PFX를 갖고 있다는 전제의 구성이며, 이것을 그대로 리포지토리의 .github/workflows/release.yml에 두면 동작합니다. 프로젝트 경로와 타임스탬프 서버 URL은 자기 환경에 맞춰 바꾸십시오.
name: release
on:
push:
tags: [ 'v*' ]
jobs:
release:
runs-on: windows-latest
# 서명 키에 닿는 job은, 승인자 있는 Environment에 묶는다(5.5절).
# 여기를 빼고 리포지토리 시크릿 그대로 두면, 쓰기 권한이 있는 사람
# (또는 탈취된 계정)이 워크플로를 고쳐 태그를 하나
# push하기만 해도 서명 키를 꺼낼 수 있습니다. 이 environment를 지정하고,
# SIGNING_PFX_BASE64와 SIGNING_PFX_PASSWORD는 Environment 쪽에 둡니다
environment: release-signing
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Test
run: dotnet test --configuration Release
- name: Publish with version from tag
shell: pwsh
run: |
$version = $env:GITHUB_REF_NAME.TrimStart('v')
dotnet publish src/MyApp/MyApp.csproj `
-c Release -o publish `
-p:Version=$version
# --- 여기부터 서명 ---
- name: Restore signing certificate
shell: pwsh
run: |
$bytes = [Convert]::FromBase64String($env:PFX_BASE64)
[IO.File]::WriteAllBytes("$env:RUNNER_TEMP\sign.pfx", $bytes)
env:
PFX_BASE64: ${{ secrets.SIGNING_PFX_BASE64 }}
- name: Sign
shell: pwsh
run: |
# signtool은 Windows SDK 포함. 경로를 고정으로 쓰지 않고 해석한다
$signtool = Get-ChildItem `
"${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
Sort-Object FullName | Select-Object -Last 1
# exe만이 아니라, 배포물에 들어가는 자사 빌드 DLL도 서명한다.
# 배포처가 App Control / AppLocker의 게시자 규칙으로 DLL도 보는 경우,
# 서명 없는 DLL이 하나라도 있으면 거기서 멈춘다.
#
# 다만 대상은 자사 빌드만으로 좁힌다. publish에는 NuGet에서 온
# 프레임워크 DLL도 들어가므로, 와일드카드로 전부 집어 올리면,
# 벤더가 서명한 DLL에 자사 인증서를 덮어쓰고(/as를 붙이지 않는
# sign은 기존 서명을 치환한다), 제3자의 미서명 DLL도
# 「자사 산출물」로 세상에 내보낸다. 게시자 기반 허용 규칙이나
# 이력 확인이 깨지므로, 이름으로 명시적으로 나열한다
$ownAssemblies = @('MyApp', 'MyApp.Core', 'MyApp.Plugins')
$targets = Get-ChildItem publish -Recurse -Include *.exe, *.dll |
Where-Object { $ownAssemblies -contains $_.BaseName } |
Select-Object -ExpandProperty FullName
if (-not $targets) { throw '서명 대상을 찾지 못했습니다. publish 내용을 확인하십시오.' }
& $signtool.FullName sign `
/f "$env:RUNNER_TEMP\sign.pfx" /p $env:PFX_PASSWORD `
/fd SHA256 /tr http://timestamp.digicert.com /td SHA256 `
$targets
if ($LASTEXITCODE -ne 0) { throw "서명에 실패했습니다 (exit $LASTEXITCODE)" }
env:
PFX_PASSWORD: ${{ secrets.SIGNING_PFX_PASSWORD }}
- name: Remove certificate
if: always()
shell: pwsh
run: Remove-Item "$env:RUNNER_TEMP\sign.pfx" -ErrorAction SilentlyContinue
# --- 여기부터 산출물 ---
- name: Zip
shell: pwsh
run: Compress-Archive -Path publish\* -DestinationPath MyApp-${{ github.ref_name }}.zip
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
files: MyApp-${{ github.ref_name }}.zip
읽을 때의 요점은 다섯 가지입니다.
environment: release-signing을 빼지 마십시오. 서명 키를 리포지토리 시크릿에 둔 채로면, 리포지토리 쓰기 권한이 있는 사람은 누구나 워크플로를 고쳐 태그를 하나 push하기만 해도 키를 꺼낼 수 있습니다. 승인자가 있는 Environment에 시크릿을 두고, 이 job만 그것을 참조하게 합니다(5.5절). 이 행이 없는 워크플로를 「완성형」으로 운용하지 마십시오.- 서명 대상은 자사 빌드만으로 좁히십시오.
publish에는 의존 패키지 DLL도 들어갑니다. 한꺼번에 서명하면, 벤더 서명을 자사 인증서로 바꾼 위에, 제3자의 미서명 DLL을 자사 산출물로 배포하게 됩니다. 어쩔 수 없이 제3자 바이너리에 서명을 더해야 한다면, 치환이 아니라/as로 추가합니다. - 서명은
dotnet publish뒤, 압축 앞입니다. zip으로 묶은 뒤에 서명해도 안의 EXE는 서명되지 않습니다. MSI나 MSIX를 만들 때도, 먼저 안의 EXE/DLL에 서명한 다음 패키지를 만들고, 마지막에 패키지 자체에 서명하는 순서입니다. - PFX는 다 쓰면 지웁니다.
if: always()를 붙여, 서명이 실패해도 삭제 스텝이 돌게 합니다. GitHub 호스팅 러너는 job마다 폐기되므로1 필수는 아니지만, 셀프 호스팅 러너로 옮기면 사고가 됩니다. - 이 워크플로는 태그 push에서만 돕니다. 포크에서 온 풀 리퀘스트에서는 실행되지 않으므로, 5.3절에서 말한 빈 시크릿 문제를 구조적으로 피할 수 있습니다.
클라우드 서명 서비스나 클라우드 HSM을 쓰는 경우에는 「Restore signing certificate」와 「Sign」 두 스텝이, 서비스 쪽이 제공하는 액션 또는 CLI 호출로 바뀔 뿐이고, 앞뒤 형태는 달라지지 않습니다.
5.5 서명 키에 닿는 워크플로를 좁히기
시크릿 다루는 법(5.3절)과 나란히 또 하나의 핵심이, 서명 키에 접근할 수 있는 워크플로를 좁히는 것입니다. 리포지토리 쓰기 권한이 있는 사람은 워크플로를 고칠 수 있으므로, 서명용 시크릿은 승인자가 있는 Environment에 두고 릴리스 워크플로만 참조하게 합니다. 서명된 바이너리는 「자사가 만들었다」는 증명 그 자체이므로, 키 취급은 자동 업데이트 배포 기반과 같은 수준의 신뢰 경계로 설계합니다(이 관점은 「자동 업데이트의 보안」을 참조).
6. 배포 형식별 CI/CD 통합 판단표
서명까지 왔으면, 마지막은 배포물 형태입니다. 방식 선정 자체는 「배포 방식 판단표」에 맡기고, 여기서는 CI/CD 관점만으로 비교합니다.
| 배포 형식 | CI에서 만들기 쉬운 정도 | CI에서 만드는 수단 | 서명 요건 | 자동 업데이트 |
|---|---|---|---|---|
| xcopy(zip 배포) | 가장 쉬움 | dotnet publish+압축만 |
EXE/DLL 서명(권장) | 없음(수동 배포) |
| xcopy+자체 업데이터 | 쉬움(빌드는). 업데이트 배포 설계는 따로 무거움 | dotnet publish+매니페스트 생성 |
EXE 서명+업데이트 파일 검증 설계가 필수 | 자체 제작(신뢰 경계 설계가 필요) |
| MSI | 중간 | WiX 등 도구를 CLI로 실행 | MSI 파일 서명(권장〜사실상 필수) | 없음(별도 배포 장치가 필요) |
| MSIX | 중간 | MSBuild / MakeAppx+signtool | 패키지 서명이 필수(미서명은 설치 불가)6 | App Installer 등으로 대응 가능 |
| ClickOnce | 까다롭다 | msbuild /target:publish+게시 프로필(dotnet CLI 비대응)7 |
매니페스트 서명+EXE 서명 | 내장(방식의 주목적) |
보탭니다.
- xcopy(zip): 3장·4장 워크플로가 거의 그대로 완성형입니다. 배포 방식이 최종적으로 무엇이든, 일단 이 형태를 한 번 통과하는 것이 지름길입니다.
- MSI: 인스톨러 정의(WiX 등)를 리포지토리에 넣고 CLI로 빌드합니다. 생성 자체보다 「MSI에 무엇을 넣을지(서비스 등록, per-machine/per-user)」 설계가 본체입니다.
- MSIX: Windows는 서명되지 않은 MSIX 설치를 허용하지 않으므로, 서명 자동화와 세트로 두지 않으면 CI화가 끝나지 않습니다.6 반대로 서명 기반이 갖춰져 있으면 CI에 올리기 쉬운 형식입니다. Microsoft Store 배포라면 Store 쪽에서 다시 서명하므로, 자체 인증서가 필요 없어지는 다른 해법도 있습니다.5
- ClickOnce: dotnet CLI에서는 게시할 수 없고, 게시 프로필(.pubxml)을 지정한
msbuild /target:publish /p:PublishProfile=...를 씁니다. IDE에서는 게시할 때마다 자동 증가하는 리비전 번호(ApplicationRevision)가 커맨드라인에서는 증가하지 않으므로7, 4장의 태그 기반으로 버전을 명시적으로 넘기는 설계가 필수입니다. 그때 주의할 점은, ClickOnce 업데이트 판정이-p:Version(어셈블리 정보)이 아니라 배포 쪽 버전(ApplicationVersion/ApplicationRevision)으로 이뤄지므로, 태그에서 만든 4부 형식 값을/p:ApplicationVersion=1.2.3.0처럼 따로 넘기지 않으면, 새 릴리스가 업데이트로 인식되지 않습니다. 구조와 적성·부적성은 「ClickOnce란 무엇인가」에서 설명합니다.
CI/CD 관점만 말하면, 「zip으로 시작해, 배포 요건이 굳으면 MSIX나 MSI를 job으로 추가한다」가 추가 작업이 적은 진행입니다. 앞 단계(빌드·테스트·버전)는 모든 형식에서 공통이므로, 나중에 배포 형식 스텝을 바꿔도 자산이 낭비되지 않습니다.
7. 테스트 자동화를 어디까지 할 것인가
마지막으로, CI 관문(필수 검사)으로 테스트를 어디까지 걸지의 선입니다.
| 테스트 층 | CI에서의 취급 | 이유 |
|---|---|---|
| 유닛 테스트(로직) | 필수 관문. 풀 리퀘스트마다 실행 | 빠르고·안정적이고·Windows 러너에서 그대로 동작 |
| 화면 없는 결합 테스트(DB·파일 I/O) | 원칙 필수. 느리면 야간 실행으로 분리 | 외부 의존 초기화에 궁리가 필요하지만 자동화 가치가 큼 |
| UI 자동 테스트(스모크) | 별도 job으로 소수만. 시작〜주요 화면 전환 정도 | 데스크톱 세션 필수라 불안정 요인이 많음 |
| UI 자동 테스트(전수) | CI 관문으로 두지 않음 | 유지 비용이 이익을 넘기기 쉬움 |
데스크톱 앱에서 자동화 가치가 가장 큰 곳은 UI가 아니라 그 아래입니다. 코드 비하인드에 업무 로직이 묻혀 있으면 유닛 테스트를 쓸 수 없으므로, 로직을 화면에서 분리하는 것 자체가 CI/CD의 전제 투자가 됩니다. UI 자동 테스트는 「시작해서, 로그인해서, 주요 화면이 열린다」 스모크로 좁히고, 야간 등 별도 트리거로 돌리는 편이 현실적입니다. 러너 위 UI 테스트에는 화면 세션·해상도·타이밍의 함정이 많고, 이 영역은 「Windows 데스크톱 앱의 UI 자동 테스트」에서 CI·무인 실행의 함정까지 포함해 다룹니다.
8. 정리
- 데스크톱 앱 CI/CD는, 출구를 「서명된 배포물의 완성」으로 정의하면 웹과 같은 도구로 구성할 수 있습니다.
- 최소 구성은
windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact입니다. 이것만으로 「개발자 PC에서만 빌드된다」는 위험이 사라집니다.12 - WinForms / WPF는
net8.0-windows등 Windows 전용 TFM이므로 Windows 러너가 전제입니다.3 - 버전은
v1.2.3태그→-p:Version주입의 태그 기반으로 일원화합니다.4 - 서명은 CI 자동화의 가장 큰 장벽입니다. OV 인증서도 HSM 보관 필수가 된 지금, Azure Artifact Signing은 일본이 대상 지역 밖이므로, CA의 클라우드 HSM 옵션을 인증서 선정 단계에서 확인하는 것이 실무의 입구입니다. PFX+시크릿 방식은 사내 CA·기존 인증서용이며, 5.4절에 완성형 워크플로를 실었습니다.510
- 태그 빌드 산출물은
softprops/action-gh-release또는 러너에 포함된gh release create로 GitHub 릴리스에 첨부해 장기 보관합니다(job에contents: write가 필요).8 - 배포 형식을 CI에 올리기 쉬운 정도는 xcopy(zip)→MSI / MSIX→ClickOnce 순서입니다. MSIX는 서명 필수6, ClickOnce는
msbuild /target:publish와 리비전 비자동 증가에 주의가 필요합니다.7 - 유닛 테스트를 CI 필수 관문으로, UI 테스트는 스모크로 좁혀 별도 job으로. 로직을 화면에서 분리하는 것이 전제 투자입니다.
관련 글
- Windows 앱 배포 방식 고르기 - MSI/MSIX/ClickOnce/xcopy/자체 업데이트
- Windows에서 「Windows에서 PC를 보호했습니다」가 나오는 이유 - SmartScreen과 코드 서명
- ClickOnce란 무엇인가 - 구조, 업데이트, 맞는 장면·맞지 않는 장면을 실무 관점으로 정리
- 자동 업데이트의 보안 설계 - HTTPS만으로는 부족한 이유
- Windows 데스크톱 앱의 UI 자동 테스트
관련 상담 영역
합동회사 고무라 소프트에서는 WinForms / WPF 앱 개발에 더해, 로컬 빌드 운용에서 CI/CD로의 이전, GitHub Actions에 의한 빌드·서명·배포 파이프라인 설계, 기존 데스크톱 앱의 테스트 가능화(로직 분리)를 다룹니다.
참고 링크
-
GitHub Docs, GitHub-hosted runners reference.
windows-latest등 러너 레이블, job마다 새 가상 머신이 할당되는 것, 퍼블릭 리포지토리에서는 표준 러너가 무료인 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, GitHub Actions and .NET. GitHub Actions에 의한 .NET CI/CD, actions/checkout·actions/setup-dotnet의 역할, 워크플로 안에서의 dotnet restore / build / test / publish 이용에 대해. ↩ ↩2
-
Microsoft Learn, MSBuild reference for .NET Desktop SDK projects. WinForms / WPF 프로젝트는
net8.0-windows처럼 Windows 고유 TFM을 지정하고,UseWindowsForms/UseWPF로 .NET 데스크톱 SDK를 활성화하는 것에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, Set assembly attributes in a project file.
Version속성에서AssemblyVersion/FileVersion(접미사 제외)와InformationalVersion이 기본 생성되는 것, .NET 8 SDK 이후SourceRevisionId(커밋 해시)가InformationalVersion에 붙는 것에 대해. ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Code signing options for Windows app developers. 2023년 6월 이후 CA/Browser Forum 요건으로 OV 인증서의 비밀 키는 HSM/하드웨어 토큰 보관이 필수인 것, EV 인증서의 최초 SmartScreen 회피가 2024년에 폐지된 것, Azure Artifact Signing(구 Trusted Signing)은 토큰 없이 GitHub Actions 등과 통합할 수 있고 이용 지역 제한이 있는 것, Store의 MSIX 배포에서는 Microsoft가 다시 서명하는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
-
Microsoft Learn, Sign an MSIX package. Windows는 MSIX 패키지에 유효한 코드 서명을 필수로 하는 것, 타임스탬프로 인증서 만료 후에도 서명 검증이 유효하게 유지되는 것에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Build .NET ClickOnce applications from the command line. .NET ClickOnce 게시에는 게시 프로필을 지정한
msbuild /target:publish가 필요한 것,ApplicationRevision이 커맨드라인 빌드에서는 자동 증가하지 않는 것에 대해. ↩ ↩2 ↩3 ↩4 -
GitHub Docs, Using GitHub CLI in workflows. GitHub CLI(
gh)가 모든 GitHub 호스팅 러너에 미리 설치되어 있는 것,gh를 쓰는 각 스텝에서 필요한 스코프를 가진 토큰을GH_TOKEN환경 변수에 설정해야 하는 것에 대해. ↩ ↩2 ↩3 -
Microsoft Learn, SignTool. SignTool은 Windows SDK에 포함되는 것, 현재 빌드에서는
/fd와/td지정이 필수이고 SHA256이 권장인 것,/tr에 의한 RFC 3161 타임스탬프 지정에 대해. ↩ ↩2 -
GitHub Docs, Using secrets in GitHub Actions. 시크릿 값의 로그 자동 은닉, 인증서 등 바이너리를 Base64로 시크릿에 저장하고 job 안에서 복원하는 절차, 포크에서 기동한 워크플로에는 시크릿이 넘어가지 않는 것에 대해. ↩ ↩2 ↩3 ↩4
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 앱의 작업 트레이 상주와 토스트 알림 ── NotifyIcon의 함정과 AppNotification 고르는 법
업무용 Windows 앱의 작업 트레이 상주와 토스트 알림 구현을 정리합니다. NotifyIcon의 올바른 사용법, Explorer 재시작 시 재등록, 토스트 API 3종의 선정 판단표, 알림이 도착하지 않는 경우까지 설명합니다.
Windows 데스크톱 앱의 UI 자동 테스트 ── UI Automation 구조와 FlaUI로 만드는 잘 깨지지 않는 테스트
WinForms/WPF 앱의 UI 자동 테스트를 Windows UI Automation 구조부터 정리합니다. FlaUI 최소 구현, AutomationId 설계와 조건 대기로 잘 깨지지 않게 하는 방법, CI 무인 실행의 함정까지 실무 관점에서 ...
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리할 점
Windows 앱 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 수정, 장치 연동, COM/ActiveX, 배포·업데이트, 유지보수를 정리하는 포인트를 설명합니다.
.NET Generic Host와 BackgroundService를 데스크톱 앱에서 쓰는 이유
Windows 도구나 상주 앱에서 시작, 주기 처리, 종료 처리, 로그, 설정, DI를 정리하기 위해 Generic Host와 BackgroundService를 어떻게 쓰는지 정리합니다.
Windows 프린터 드라이버 제공 종료 ── 업무 앱의 장표·라벨 인쇄는 어떻게 대비할 것인가
Microsoft는 v3/v4 프린터 드라이버의 제공 종료를 단계적으로 진행하고 있으며, 2026년 7월부터는 IPP 클래스 드라이버가 우선됩니다. Windows protected print mode에서 무엇이 사라지는지, 업무 앱의 장표·라벨 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
Windows 소프트웨어 유지 보수 & 현대화
기존 Windows 소프트웨어에 대한 단계적 업그레이드, 기능 추가, 64비트 대응, 유지 보수 가능한 재구조화를 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- WinForms / WPF 앱 빌드는 GitHub Actions의 어느 러너에서 돌려야 합니까?
- windows-latest와 같은 Windows 러너를 사용합니다. WinForms / WPF 프로젝트는 net8.0-windows처럼 Windows 전용 타깃 프레임워크를 대상으로 하며, 빌드 산출물의 동작 확인이나 dotnet test 실행에는 Windows 환경이 필요합니다. GitHub 호스팅 러너는 job마다 새 가상 머신이 할당되므로, 개발자 PC에 묶이지 않는 재현 가능한 빌드를 얻을 수 있습니다. 퍼블릭 리포지토리에서는 표준 러너가 무료이고, 프라이빗 리포지토리에서는 분 단위로 과금됩니다.
- 코드 서명은 CI에서 완전히 자동화할 수 있습니까?
- 인증서를 어떻게 들고 있느냐에 따라 달라집니다. 예전처럼 PFX 파일을 시크릿에 두고 signtool로 서명하는 방식은, 2023년 6월 이후 CA/Browser Forum 요건으로 공인 OV 인증서의 비밀 키가 HSM(하드웨어) 보관 필수가 되었기 때문에, 새로 발급받는 인증서에서는 원칙적으로 쓸 수 없습니다. USB 토큰형 인증서는 클라우드 러너에 꽂을 수 없으므로, CI에서 완전 자동화하려면 Azure Artifact Signing(구 Trusted Signing) 같은 클라우드 서명 서비스나 CA가 제공하는 클라우드 HSM을 거치는 편이 현실적입니다. 토큰 운용을 유지한다면, 서명 단계만 로컬 또는 셀프 호스팅 러너에 남기는 구성이 됩니다.
- 어디부터 자동화를 시작해야 합니까?
- 빌드+테스트 자동화만 먼저 넣어야 합니다. push할 때마다 windows-latest 러너에서 dotnet build / dotnet test가 돌아가게만 해도, 「개발자 PC에서만 빌드된다」, 「머지로 빌드가 깨진 사실을 배포 직전까지 모른다」는 가장 큰 위험이 사라집니다. 서명·인스톨러 작성·배포 자동화는 그다음에 단계적으로 더하면 되고, 처음부터 전부를 한 번에 짜려다 보면 서명에서 멈추기 쉽습니다.
- 배포 형식(MSI / MSIX / ClickOnce / xcopy)에 따라 CI/CD를 올리기 쉬운 정도가 달라집니까?
- 크게 달라집니다. xcopy 배포(zip)는 dotnet publish 출력을 압축하기만 하면 되므로 가장 간단합니다. MSIX는 MSBuild와 signtool로 CI에 올릴 수 있지만, 패키지 서명이 필수입니다. MSI는 WiX 같은 도구를 CI에서 호출하는 형태로 자동화할 수 있습니다. ClickOnce는 dotnet CLI로 게시할 수 없고, msbuild /target:publish와 게시 프로필의 조합이 필요하며, 커맨드라인에서는 리비전 번호가 자동 증가하지 않는다는 점에도 주의가 필요합니다. 배포 방식을 정하는 단계에서 CI에 올리기 쉬운지도 판단 재료에 넣어야 합니다.