CI/CD для WinForms / WPF: сборка, подпись и распространение в GitHub Actions
· Обновлено: · Го Комура · CI/CD, GitHub Actions, WinForms, WPF, C#, .NET, Подпись кода, MSIX, Deployment, Разработка Windows, Таблица решений
История изменений (1 обновлений, последнее 30 Aug 2026)
Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.
- Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
- Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21620050)
Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.
Го Комура (2026). CI/CD для WinForms / WPF: сборка, подпись и распространение в GitHub Actions. KomuraSoft LLC. https://comcomponent.com/ru/blog/winforms-wpf-cicd-github-actions/
- DOI (зарегистрированный архив)
- 10.5281/zenodo.21620050
- DOI (последняя зарегистрированная версия)
- 10.5281/zenodo.21620051
«Релиз можно собрать только на компьютере вот этого человека» — фраза, которую на консультациях по бизнес-приложениям на WinForms и WPF слышишь очень часто. Кто-то делает Release-сборку в локальной Visual Studio, упаковывает в zip и кладёт в общую папку. Работает, но никто не ответит, получится ли выпустить исправление в день, когда этот разработчик в отпуске.
Про CI/CD для веб-приложений материалов полно, а стоит перейти к десктопным — и их резко становится меньше. Это отчасти справедливо: «точка развёртывания — не сервер, а компьютер заказчика» — принципиальное отличие, из-за которого веб-статью нельзя просто скопировать. Тем не менее автоматизацию сборки и тестов для десктопного приложения можно поставить почти с теми же усилиями, что и для веба. Дальше начинается препятствие — подпись и распространение, и там практичный путь зависит от формата дистрибутива.
В этом блоге выбор формата распространения разобран в статье «Таблица решений по способам распространения Windows-приложений», а подход к подписи — в «SmartScreen и подпись кода». Опираясь на эти две статьи, здесь с практической точки зрения разбираем, насколько далеко можно автоматизировать сборку, тесты, нумерацию версий, подпись и создание дистрибутива WinForms / WPF-приложения с помощью GitHub Actions.
Кому адресована статья и какие условия предполагаются
Чтобы сразу примерить материал к своей ситуации, сначала перечислим, из каких условий исходит статья.
- Кому: разработчики и команды, которые собирают Windows-десктоп на WinForms / WPF в локальной Visual Studio и так же распространяют. Опыт CI/CD не обязателен.
- Репозиторий: исходники лежат в репозитории GitHub (публичный или приватный — не важно). На публичных репозиториях стандартные раннеры бесплатны, на приватных время выполнения тарифицируется по минутам.1
- Формат проекта: YAML в статье рассчитан на csproj формата .NET SDK (
net8.0-windowsи подобные). Для старого csproj на .NET Framework 4.x подход тот же, только вместоdotnet buildиспользуют MSBuild и NuGet CLI (раздел 3). - Подпись: раздел 5 начинается с вопроса, как сейчас готовить сертификат. Вывод зависит от того, есть ли уже PFX-файл или внутренний CA, или вы только собираетесь получать публичный сертификат — читайте, сверяясь со своей ситуацией.
- Цель: не полностью автоматическое распространение, а сначала состояние «сборку и тесты можно воспроизвести на любом компьютере и забрать артефакт» (раздел 3). Подпись и распространение добавляют поэтапно.
Общий вид конвейера такой. Насколько далеко его автоматизировать — в разделе 2.
[повседневная работа] push в main / pull request
└→ checkout → setup-dotnet → build → test → publish → upload-artifact (раздел 3)
[релиз] push тега v1.2.3
└→ checkout → setup-dotnet → test
→ publish с версией из тега (раздел 4)
→ подпись (signtool / облачный сервис подписи) (раздел 5)
→ сборка дистрибутива (zip / MSI / MSIX / ClickOnce) (раздел 6)
→ вложение в GitHub Release (долгое хранение) (раздел 4)
1. Главное в начале
- Главный риск — состояние «собрать можно только на компьютере одного разработчика». Первая цель CI/CD — не полная автоматизация распространения, а воспроизводимая сборка без привязки к чьему-либо компьютеру.
- Минимальный набор — только автоматизация сборки и тестов — уже даёт заметную пользу. Его можно собрать в одном YAML:
windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact.12 - WinForms / WPF предполагают Windows-раннер. Проекты нацелены на Windows-only TFM вроде
net8.0-windows,3 поэтому если тесты тоже гонять в CI, нужна среда Windows. - Практичный способ нумеровать версии — релизы по тегам. Push тега
v1.2.3запускает релизную сборку, а значение из тега передаётся в свойство MSBuildVersion.4 - Подпись — главное препятствие автоматизации. С июня 2023 года закрытый ключ публичного OV-сертификата обязан храниться на HSM, поэтому прежний стандарт «PFX в секрете и signtool» как есть уже не работает. Облачный сервис, который проще всего стыковать с CI, — Azure Artifact Signing (бывший Trusted Signing) — Японию в список регионов не включает, поэтому для японских разработчиков реалистичный путь — облачный HSM от CA либо схема, где этап подписи остаётся на своей стороне (раздел 5.2).5
- Насколько удобно встроить формат в CI, сильно зависит от дистрибутива. xcopy (zip) проще всего, MSIX требует обязательной подписи,6 MSI стыкуется через CLI вроде WiX, ClickOnce требует
msbuild /target:publishи ведёт себя капризно.7 - Не делайте UI-автотесты обязательным gate CI. Практичный путь: юнит-тесты обязательны, UI-тесты сужают до дымовых и гоняют отдельным job.
На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle
2. Чем CI/CD десктопного приложения отличается от веба
Сначала разберём, почему шаблон CI/CD веб-приложения (push → сборка → тесты → деплой на сервер) нельзя перенести без изменений.
| Аспект | Веб-приложение | Десктопное приложение WinForms / WPF |
|---|---|---|
| Куда деплоим | Сервер под своим управлением | Компьютеры заказчика и площадок (вне вашего контроля) |
| Единица распространения | Одновременное переключение на сервере | MSI / MSIX / ClickOnce / zip и другое; момент установки зависит от другой стороны |
| Откат | Можно откатить на сервере | С уже обновлённых компьютеров легко не вернуть. Нужно хранить старые инсталляторы |
| Подпись | Обычно не нужна (TLS на стороне инфраструктуры) | Подпись кода исполняемого файла и пакета практически обязательна |
| Среда сборки | Часто укладывается в Linux-раннер | Предполагается Windows-раннер |
| Тесты | Часто обходятся без графической сессии | Юнит-тесты те же; UI-тестам нужна десктопная сессия |
| Что значит «деплой» | Доведение до продакшена | Зона ответственности CI заканчивается на «дистрибутив готов». Установка — отдельный этап |
Важна последняя строка. Для десктопного приложения выход конвейера CI/CD — не «доведение до продакшена», а «подписанный дистрибутив лежит там, откуда его в любой момент можно забрать». Всё дальше (развёртывание у заказчика, автообновление) — вопрос проектирования распространения, разобранный в статье «Таблица решений по способам распространения». Если так определить точку выхода, CI/CD для десктопа собирается теми же инструментами, что и для веба.
3. Минимальный набор — сборка и тесты в GitHub Actions
Именно это стоит включить первым. GitHub-hosted runner на каждый job выделяет новую виртуальную машину,1 поэтому на каждый push сборка и тесты идут на чистой Windows, и зависимость от «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 замените на путь к проекту в своём репозитории. Дальше в статье тот же путь используется как пример. Если в корне репозитория лежат только solution и один csproj, путь можно опустить: dotnet publish -c Release -o publish.
Три уточнения.
Во-первых, базовый вариант — runs-on: windows-latest. Проект WinForms / WPF имеет TargetFramework вида net8.0-windows (Windows-only TFM) и включает UseWindowsForms или UseWPF — это проект .NET Desktop SDK.3 Строго говоря, если нужна только компиляция, собрать можно и на Linux-раннере, включив EnableWindowsTargeting. Но шаги с реальным запуском, вроде dotnet test, требуют среды Windows, поэтому в этой схеме, где тесты идут в том же job, логично брать Windows-раннер. Для .NET Framework 4.x (csproj старого формата) вместо dotnet build используют MSBuild и NuGet CLI; оба уже стоят на Windows-раннере, подход тот же.
Во-вторых, артефакт обязательно сохраняйте через actions/upload-artifact. «Полный набор файлов этой сборки можно забрать из GitHub» — это и есть выход из привязки к чужому компьютеру. Срочная сборка для проверки тоже сводится к скачиванию zip с экрана Actions.
В-третьих, на этом этапе подпись и распространение ещё не делаем. Уже этот минимальный набор даёт две гарантии: «main всегда можно собрать и прогнать тесты» и «любой может забрать один и тот же артефакт». По опыту автора этого обычно хватает, чтобы снять большую часть проблем небольшой команды.
4. Автоматическая нумерация версий — релизы по тегам
Следующий шаг — проблема «а какая версия у этого zip?». При локальных сборках классическая ошибка — забыть поменять Version в csproj, и одна и та же 1.0.0 живёт уже много поколений. Практичный путь — релизы по тегам: на коммит, который нужно выпустить, ставят тег вроде v1.2.3, это запускает workflow, а версия из имени тега передаётся в сборку.
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 тега не запускает workflow сборки и тестов из раздела 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
Если передать значение как свойство MSBuild, например -p:Version=1.2.3, в проекте .NET SDK по умолчанию AssemblyVersion и FileVersion берутся из префикса Version (часть без суффикса), а InformationalVersion — из самого Version.4 В csproj остаётся только черновое значение для разработки, а официальную версию релиза хранит только тег.
Есть оговорка. У артефактов actions/upload-artifact есть срок хранения на уровне репозитория (по умолчанию 90 дней), после которого они исчезают. Десктопному приложению для отката нужны старые инсталляторы надолго, поэтому артефакты тегированной сборки прикрепляют к GitHub Release или кладут в другое постоянное место, а артефакт Actions считают временной передачей.
Прикрепить файл к GitHub Release можно специальным action или GitHub CLI (gh), который уже стоит на раннере.8 В обоих случаях job нужен permission 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: отдельный action
- 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-hosted runner, но в каждом шаге в переменную окружения GH_TOKEN нужно передать токен с нужным scope.8
Плюс в том, что эксплуатация остаётся внутри Git. «Какому коммиту соответствует 1.2.3 у заказчика» однозначно задаёт тег, а файловая версия в свойствах EXE механически совпадает с тегом Git. Кроме того, начиная с .NET 8 SDK к InformationalVersion по умолчанию добавляется хэш коммита Git (SourceRevisionId),4 и если показать его на экране версии, коммит можно определить прямо по артефакту.
5. Подпись кода в CI — здесь главное препятствие
Команды, которым удалось гладко автоматизировать сборку и версии, почти всегда останавливаются на подписи. Почему для Windows-приложения вне Store подпись кода практически обязательна (SmartScreen, корпоративные средства защиты, обнаружение подделки), разобрано в статье «SmartScreen и подпись кода». Здесь сосредоточимся только на том, где в CI это выполнять и как.
5.1 Базовый вызов signtool
Сама подпись — одна команда. signtool входит в Windows SDK и доступен на Windows-раннерах GitHub. В текущих 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 и подобным | Есть ограничения по странам и регионам, Япония не входит (для юрлиц — США, Канада, ЕС, Великобритания; для физлиц — только США и Канада). Подробности ниже5 |
| OV-сертификат (впервые выпущенный с июня 2023 года) | Обязателен HSM / USB-токен | Токен в раннер не вставить, поэтому как есть нельзя. Можно через облачный HSM от CA | По требованиям CA/Browser Forum5 |
| EV-сертификат | HSM / USB-токен | То же | Эффект мгновенного доверия SmartScreen отменён в 2024 году. С точки зрения эксплуатации подписи считайте наравне с OV5 |
| Старый PFX-файл (выпущенный раньше, внутренний CA, самоподписанный) | Файл | Хранить в секрете в Base64 и восстанавливать (см. ниже) | Для новой публичной выдачи в таком виде сертификат, как правило, уже не получить |
То есть схема «PFX в секрете GitHub и подпись через signtool», которую часто находят поиском, для внутреннего CA или уже имеющегося PFX по-прежнему работает, но если публичный сертификат вы только собираетесь получать, эта предпосылка уже не держится. Собирая конвейер с нуля, реалистичнее сразу смотреть на облачный сервис подписи, который умеет стыковаться с CI.5 Если уже работаете с USB-токеном, получается компромисс: этап подписи остаётся на своей машине или на self-hosted runner со вставленным токеном.
Главный вопрос для разработчика в Японии — доступен ли Azure Artifact Signing
В первой строке таблицы сказано «есть ограничения по странам и регионам». Для читателя в Японии это главный критерий выбора, поэтому разберём отдельно.
Документация Microsoft прямо указывает: Azure Artifact Signing (бывший Trusted Signing) доступен юридическим лицам в США, Канаде, ЕС и Великобритании, а индивидуальным разработчикам — только в США и Канаде.5 То есть японские юрлица и разработчики, живущие в Японии, сейчас вне списка. Если перенести общее «облачная подпись — основной путь» без этой оговорки, процесс остановится уже на создании учётной записи. Варианты с учётом ограничения такие.
| Ситуация | Реалистичный выбор |
|---|---|
| Собираетесь получать публичный сертификат (японское юрлицо) | OV-сертификат + облачный HSM от CA. С июня 2023 года закрытый ключ OV обязан храниться на HSM или аппаратном токене, но многие CA помимо USB-токена предлагают облачный HSM — тогда подпись можно вызывать из CI.5 Если планируете вынести подпись в CI, ещё на этапе выбора сертификата спросите у CA, есть ли облачный HSM. После покупки токена схему уже не сменить |
| Уже работаете с USB-токеном | Этап подписи оставляете на своей машине с токеном или на self-hosted runner. Сборку, тесты и нумерацию версий автоматизируете на GitHub-hosted runner, а последнюю подпись оставляете человеку |
| Можно распространять через Microsoft Store (MSIX) | Store переподписывает пакет со стороны Microsoft, свой сертификат не нужен.5 Если формат распространения ещё можно пересмотреть, это самый короткий путь убрать проблему подписи (но если в Store идёт MSI/EXE-инсталлятор, подпись издателя всё равно нужна) |
| Проект с открытым исходным кодом | SignPath Foundation бесплатно подписывает код OSS-проектов, которые проходят их условия.5 |
| Только внутреннее распространение | Достаточно сертификата внутреннего CA и существующей PFX-схемы (раздел 5.3). Если сертификат можно раздать как доверенный корень через групповую политику или Intune, публичный сертификат не нужен |
Регионы Azure Artifact Signing со временем могут расшириться, поэтому когда фиксируете схему подписи в CI/CD, проверяйте актуальный список регионов по первоисточнику.5
5.3 На что смотреть в секретах
Проверенный приём, если в CI выносите PFX-схему (внутренний CA, уже имеющийся сертификат).
- PFX кладите в секрет GitHub как строку Base64 и внутри job восстанавливайте в файл. Так GitHub Docs рекомендует хранить двоичные данные в секретах.10
- Пароль держите в отдельном секрете. Значения секретов в логах маскируются автоматически,10 но производные значения после обработки уже не защищены. Не передавайте их в переменные окружения никуда, кроме шага подписи.
- В pull request из форка секреты не попадают (кроме
GITHUB_TOKEN).10 Однако сам job всё равно запускается с пустыми секретами, поэтому шаг восстановления упадёт на Base64-декодировании пустой строки. Либо вынесите подпись в релизный workflow по тегу, как в разделе 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 Готовый workflow для PFX-схемы
Ниже фрагменты из предыдущих разделов (версия из тега, восстановление PFX, вызов signtool, публикация артефакта) собраны в один workflow. Схема рассчитана на внутренний CA или уже имеющийся PFX; если положить её в .github/workflows/release.yml, она заработает. Путь к проекту и URL сервера меток времени замените на свои.
name: release
on:
push:
tags: [ 'v*' ]
jobs:
release:
runs-on: windows-latest
# Job, который работает с ключом подписи, привязываем к Environment
# с утверждающими (раздел 5.5). Если эту строку убрать и оставить
# секреты на уровне репозитория, любой с правом записи
# (или скомпрометированная учётная запись) сможет переписать workflow,
# сделать 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 попадают
# и DLL из NuGet, и из фреймворка; если схватить всё маской,
# мы перезапишем вендорскую подпись своим сертификатом
# (sign без /as заменяет существующую подпись) и выдадим
# неподписанные чужие 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. Если ключ подписи лежит в секретах репозитория, любой с правом записи может переписать workflow, сделать push одного тега и забрать ключ. Секреты кладут в Environment с утверждающими, и к ним обращается только этот job (раздел 5.5). Не эксплуатируйте как «готовый» workflow, в котором этой строки нет. - Подписывайте только собственные сборки. В
publishпопадают и DLL зависимостей. Если подписать всё подряд, вы замените вендорскую подпись своим сертификатом и раздадите неподписанные чужие DLL как свой артефакт. Если чужой двоичный файл всё же нужно подписать, добавляйте подпись через/as, а не заменяйте её. - Подпись — после
dotnet publish, до сжатия. Подписать zip бесполезно: EXE внутри останется без подписи. Для MSI и MSIX сначала подписывают EXE/DLL внутри, затем собирают пакет и в конце подписывают сам пакет. - PFX после использования удаляйте. С
if: always()шаг удаления выполнится даже если подпись упала. GitHub-hosted runner уничтожается после каждого job,1 так что это не обязательно, но при переносе на self-hosted runner без удаления легко получить инцидент. - Этот workflow срабатывает только на push тега. На pull request из форка он не запускается, поэтому проблема пустых секретов из раздела 5.3 здесь снята самой структурой.
Если используете облачный сервис подписи или облачный HSM, шаги «Restore signing certificate» и «Sign» заменяются action или вызовом CLI, который даёт сервис. Остальная форма не меняется.
5.5 Сужайте набор workflow, которые видят ключ подписи
Рядом с обращением с секретами (раздел 5.3) вторая острая точка — ограничить набор workflow, у которых есть доступ к ключу подписи. Кто может писать в репозиторий, может и переписать workflow, поэтому секреты подписи кладут в Environment с утверждающими и дают к ним доступ только релизному workflow. Подписанный двоичный файл сам по себе доказывает «это собрали мы», поэтому ключ проектируют как ту же границу доверия, что и инфраструктуру доставки автообновлений (этот угол зрения — в статье «Безопасность автообновления»).
6. Таблица интеграции CI/CD по форматам распространения
Когда подпись уже есть, остаётся форма дистрибутива. Сам выбор формата оставляем статье «Таблица решений по способам распространения», а здесь сравниваем только с точки зрения CI/CD.
| Формат распространения | Насколько просто собрать в CI | Как собирать в CI | Требования к подписи | Автообновление |
|---|---|---|---|---|
| xcopy (распространение zip) | Проще всего | Только dotnet publish + сжатие |
Подпись EXE/DLL (рекомендуется) | Нет (ручная установка) |
| xcopy + свой updater | Сборка простая. Проектирование доставки обновлений — отдельная тяжёлая задача | dotnet publish + генерация манифеста |
Подпись EXE + обязательно спроектировать проверку файлов обновления | Свой (нужно проектировать границу доверия) |
| MSI | Средне | Запуск CLI такого инструмента, как WiX | Подпись MSI-файла (рекомендуется, местами фактически обязательна) | Нет (нужен отдельный механизм распространения) |
| MSIX | Средне | MSBuild / MakeAppx + signtool | Подпись пакета обязательна (неподписанный пакет установить нельзя)6 | Можно через App Installer и подобное |
| ClickOnce | Ведёт себя капризно | msbuild /target:publish + профиль публикации (dotnet CLI не поддерживает)7 |
Подпись манифеста + подпись EXE | Встроено (главное назначение формата) |
Дополним.
- xcopy (zip): workflow из разделов 3 и 4 почти и есть готовая форма. Каким бы ни оказался итоговый формат, сначала дойти до этого — самый короткий путь.
- MSI: определение инсталлятора (WiX и т. п.) кладут в репозиторий и собирают через CLI. Суть не столько в самой генерации, сколько в том, «что класть в MSI» (регистрация службы, per-machine/per-user).
- MSIX: Windows не разрешает ставить неподписанный MSIX, поэтому без автоматизации подписи CI для этого формата не замыкается.6 Зато если инфраструктура подписи уже есть, формат в CI ложится хорошо. Другой вариант: при распространении через Microsoft Store пакет переподписывает Store, и свой сертификат не нужен.5
- ClickOnce: через dotnet CLI опубликовать нельзя — нужен
msbuild /target:publish /p:PublishProfile=...с профилем публикации (.pubxml). Номер редакции (ApplicationRevision), который в IDE увеличивается при каждой публикации, из командной строки не растёт,7 поэтому явную передачу версии через релизы по тегам из раздела 4 здесь нельзя опускать. При этом обновление ClickOnce смотрит не на-p:Version(сведения о сборке), а на версию со стороны развёртывания (ApplicationVersion/ApplicationRevision). Если отдельно не передать четырёхчастное значение из тега, например/p:ApplicationVersion=1.2.3.0, новый релиз не распознается как обновление. Механизм и где формат уместен — в статье «Что такое ClickOnce».
Если смотреть только на CI/CD, путь с наименьшим лишним объёмом — «начать с zip, а когда требования к распространению прояснятся, добавить MSIX или MSI отдельным job». Предыдущие этапы (сборка, тесты, версии) общие для всех форматов, поэтому замена шага распространения позже не выбрасывает уже сделанную работу.
7. Насколько далеко автоматизировать тесты
Напоследок — где провести границу: какие тесты делать обязательным gate CI.
| Слой тестов | Как обращаться в CI | Почему |
|---|---|---|
| Юнит-тесты (логика) | Обязательный gate. Запускать на каждый pull request | Быстро, стабильно, на Windows-раннере работают как есть |
| Интеграционные тесты без экрана (БД, файловый ввод-вывод) | В принципе обязательны; если медленные — вынести в ночной прогон | Инициализация внешних зависимостей требует усилий, но отдача от автоматизации высокая |
| UI-автотесты (дым) | Небольшое число, отдельным job. Запуск и переход по основным экранам | Нужна десктопная сессия, много источников нестабильности |
| UI-автотесты (полное покрытие) | Не делать gate CI | Стоимость поддержки обычно перевешивает пользу |
У десктопного приложения автоматизация лучше всего окупается не на UI, а под ним. Если бизнес-логика зарыта в code-behind, юнит-тесты не написать, поэтому отделение логики от экрана само по себе — предварительная инвестиция в CI/CD. Практичный путь: сузить UI-автотесты до дыма «запустился, вошёл, открылись основные экраны» и гонять отдельным триггером, например ночью. На раннере UI-тесты легко ломаются из-за сессии экрана, разрешения и тайминга; эта область, включая ловушки CI и запуска без оператора, разобрана в статье «UI-автотесты десктопных приложений Windows».
8. Итог
- CI/CD десктопного приложения собирается теми же инструментами, что и для веба, если точку выхода определить как «готов подписанный дистрибутив».
- Минимальный набор —
windows-latest+actions/checkout+actions/setup-dotnet+dotnet build / test+actions/upload-artifact. Уже этого достаточно, чтобы снять риск «собрать можно только на компьютере одного разработчика».12 - WinForms / WPF предполагают Windows-раннер, потому что нацелены на Windows-only TFM вроде
net8.0-windows.3 - Версию централизуйте релизами по тегам: тег
v1.2.3→ передача в-p:Version.4 - Подпись — главное препятствие автоматизации CI. Теперь, когда и OV-сертификаты обязаны храниться на HSM, а Япония вне регионов Azure Artifact Signing, на этапе выбора сертификата спрашивайте у CA про облачный HSM. Схема PFX + секрет годится для внутреннего CA и уже имеющегося сертификата; готовый workflow — в разделе 5.4.510
- Артефакты тегированной сборки прикрепляйте к GitHub Release через
softprops/action-gh-releaseили предустановленныйgh release createи храните долго (job нуженcontents: write).8 - Насколько формат удобно встроить в CI, по убыванию: xcopy (zip) → MSI / MSIX → ClickOnce. MSIX требует обязательной подписи,6 у ClickOnce смотрите на
msbuild /target:publishи на то, что номер редакции сам не увеличивается.7 - Юнит-тесты — обязательный gate CI, UI-тесты сужайте до дыма и гоняйте отдельным job. Отделение логики от экрана — предварительная инвестиция.
Связанные статьи
- Как выбрать способ распространения Windows-приложения — MSI / MSIX / ClickOnce / xcopy / свой updater
- Почему в Windows появляется «Windows защитила ваш компьютер» — SmartScreen и подпись кода
- Что такое ClickOnce — механизм, обновления и где формат уместен, с практической точки зрения
- Безопасность автообновления — почему одного HTTPS недостаточно
- UI-автотесты десктопных приложений Windows
Смежные темы консультаций
В Komura Software LLC помимо разработки приложений WinForms / WPF занимаемся переходом с локальных сборок на CI/CD, проектированием конвейеров сборки, подписи и распространения на GitHub Actions и тем, чтобы существующие десктопные приложения стало можно тестировать (отделение логики).
- Разработка приложений для Windows
- Доработка и сопровождение существующего ПО для Windows
- Техническая консультация и ревью проекта
- Контакты
Справочные материалы
-
GitHub Docs, GitHub-hosted runners reference. О метках раннеров вроде
windows-latest, о том, что каждому job выделяется новая виртуальная машина, и о том, что на публичных репозиториях стандартные раннеры бесплатны. ↩ ↩2 ↩3 ↩4 ↩5 -
Microsoft Learn, GitHub Actions and .NET. О CI/CD для .NET через GitHub Actions, о роли actions/checkout и actions/setup-dotnet и об использовании dotnet restore / build / test / publish внутри workflow. ↩ ↩2
-
Microsoft Learn, MSBuild reference for .NET Desktop SDK projects. О том, что проекты WinForms / WPF указывают Windows-specific TFM вроде
net8.0-windowsи включают .NET Desktop SDK черезUseWindowsForms/UseWPF. ↩ ↩2 ↩3 -
Microsoft Learn, Set assembly attributes in a project file. О том, что из свойства
Versionпо умолчанию получаютсяAssemblyVersion/FileVersion(без суффикса) иInformationalVersion, и о том, что начиная с .NET 8 SDK кInformationalVersionдобавляетсяSourceRevisionId(хэш коммита). ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Code signing options for Windows app developers. О том, что с июня 2023 года по требованиям CA/Browser Forum закрытый ключ OV-сертификата обязан храниться на HSM / аппаратном токене, об отмене в 2024 году обхода первичного SmartScreen для EV-сертификатов, о том, что Azure Artifact Signing (бывший Trusted Signing) не требует токена, стыкуется с GitHub Actions и подобным, но ограничен по регионам, и о переподписи Microsoft для MSIX, распространяемых через Store. ↩ ↩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. О том, что публикация ClickOnce в .NET требует
msbuild /target:publishс профилем публикации, и о том, чтоApplicationRevisionпри сборке из командной строки сам не увеличивается. ↩ ↩2 ↩3 ↩4 -
GitHub Docs, Using GitHub CLI in workflows. О том, что GitHub CLI (
gh) предустановлен на всех GitHub-hosted runner и что в каждом шаге сghв переменную окруженияGH_TOKENнужно задать токен с нужным scope. ↩ ↩2 ↩3 -
Microsoft Learn, SignTool. О том, что SignTool входит в Windows SDK, что в текущих сборках обязательны
/fdи/tdс рекомендуемым SHA256, и об указании метки времени RFC 3161 через/tr. ↩ ↩2 -
GitHub Docs, Using secrets in GitHub Actions. Об автоматической маскировке значений секретов в логах, о хранении двоичных данных вроде сертификатов в секрете в Base64 и восстановлении внутри job, и о том, что секреты не передаются в workflow, запущенные из форка. ↩ ↩2 ↩3 ↩4
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
UI-автотесты десктопных приложений Windows — UI Automation и устойчивые тесты на FlaUI
Разбираем UI-автотесты WinForms и WPF с устройства Windows UI Automation. Минимальная реализация на FlaUI, AutomationId и ожидание по усл...
Зачем использовать .NET Generic Host и BackgroundService в десктопных приложениях
Как с помощью Generic Host и BackgroundService собрать запуск, периодическую работу, завершение, логи, конфигурацию и DI в Windows-инстру...
Значок в области уведомлений и toast в Windows-приложении: ошибки NotifyIcon и выбор AppNotification
Разбираем, как держать бизнес-приложение Windows в области уведомлений и показывать toast. Правильная работа с NotifyIcon, повторная реги...
Локализация WinForms и WPF: resx, спутниковые сборки, переключение культуры
Разбираем локализацию десктопных приложений Windows: чем CurrentCulture отличается от CurrentUICulture, как устроены resx и спутниковые с...
Печать и PDF в Windows-приложениях: PrintDocument, WPF и библиотеки отчётов
Печать WinForms через PrintDocument, печать WPF через FlowDocument и FixedDocument и варианты вывода PDF сведены в таблицу по требованиям...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Поток UI и таймеры
Поток UI WPF / WinForms, асинхронные операции, Dispatcher и проектирование таймеров.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Сопровождение и модернизация ПО Windows
Безопасно добавляем функции, сопровождаем и поэтапно модернизируем существующее ПО Windows.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- На каком раннере GitHub Actions запускать сборку приложения WinForms / WPF?
- Нужен Windows-раннер, например windows-latest. Проекты WinForms / WPF нацелены на Windows-only TFM вроде net8.0-windows, поэтому и проверка собранного приложения, и запуск тестов через dotnet test требуют среды Windows. GitHub-hosted runner на каждый job выделяет новую виртуальную машину, поэтому сборка воспроизводится независимо от компьютера разработчика. На публичных репозиториях стандартные раннеры бесплатны, на приватных время тарифицируется по минутам.
- Можно ли полностью автоматизировать подпись кода в CI?
- Зависит от того, как хранится сертификат. Прежний приём — положить PFX в секрет и подписывать через signtool — для вновь полученных сертификатов в большинстве случаев уже не работает: с июня 2023 года требования CA/Browser Forum обязали хранить закрытый ключ публичного OV-сертификата на HSM. USB-токен в облачный раннер не вставить, поэтому полная автоматизация в CI реалистична через облачный сервис подписи вроде Azure Artifact Signing (бывший Trusted Signing) или через облачный HSM, который даёт CA. Если остаётесь на токене, этап подписи оставляют на своей машине или на self-hosted runner.
- С чего начинать автоматизацию?
- Сначала стоит включить только автоматизацию сборки и тестов. Уже то, что на каждый push на раннере windows-latest выполняются dotnet build / dotnet test, снимает главный риск: «собрать можно только на компьютере одного разработчика» и «о сломанной после merge сборке узнают перед самым релизом». Подпись, сборку инсталлятора и распространение добавляют поэтапно. Попытка сразу автоматизировать всё обычно упирается именно в подпись.
- Зависит ли удобство CI/CD от формата распространения (MSI / MSIX / ClickOnce / xcopy)?
- Да, и сильно. xcopy (zip) проще всего: достаточно сжать вывод dotnet publish. MSIX можно встроить в CI через MSBuild и signtool, но подпись пакета обязательна. MSI автоматизируется вызовом из CI таких инструментов, как WiX. ClickOnce через dotnet CLI опубликовать нельзя: нужна связка msbuild /target:publish и профиля публикации, плюс из командной строки номер редакции сам не увеличивается. Насколько формат удобно встроить в CI, стоит учитывать уже при выборе способа распространения.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.