From e3ffe139dc921740ebf9eab2e6f0a7ef12e1623a Mon Sep 17 00:00:00 2001 From: nextestudios <47460003+nextestudios@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:32:51 -0300 Subject: [PATCH] chore: WinGet manifest generator and validation workflow (#273) Manual workflow that builds the manifests from a published release and runs winget validate. It never submits anything to microsoft/winget-pkgs. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/winget-manifest.yml | 25 +++++++ build/New-WingetManifest.ps1 | 102 ++++++++++++++++++++++++++ docs/build-and-release.md | 10 +++ 3 files changed, 137 insertions(+) create mode 100644 .github/workflows/winget-manifest.yml create mode 100644 build/New-WingetManifest.ps1 diff --git a/.github/workflows/winget-manifest.yml b/.github/workflows/winget-manifest.yml new file mode 100644 index 00000000..5c555b0a --- /dev/null +++ b/.github/workflows/winget-manifest.yml @@ -0,0 +1,25 @@ +name: WinGet manifest + +# Gera e valida os manifestos do WinGet de uma release já publicada (#273). Nunca envia nada ao repositório da Microsoft. +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag already published (e.g. v0.12.0-alpha.1)" + required: true + +permissions: + contents: read + +jobs: + manifest: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Generate and validate manifests + shell: pwsh + run: .\build\New-WingetManifest.ps1 -Tag "${{ inputs.tag }}" -OutDir artifacts\winget + - uses: actions/upload-artifact@v4 + with: + name: winget-manifests + path: artifacts/winget diff --git a/build/New-WingetManifest.ps1 b/build/New-WingetManifest.ps1 new file mode 100644 index 00000000..be005744 --- /dev/null +++ b/build/New-WingetManifest.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS + Gera os manifestos do WinGet (#273) de uma release publicada do ControlFS e, onde o winget existir, valida. + Não envia nada para o repositório da Microsoft: o envio é um passo manual e consciente (docs/build-and-release.md). +.EXAMPLE + .\build\New-WingetManifest.ps1 -Tag v0.12.0-alpha.1 -OutDir artifacts\winget +#> +param( + [Parameter(Mandatory)][string]$Tag, + [string]$OutDir = "artifacts\winget", + [string]$Repo = "nextestudios/ControlFS" +) +$ErrorActionPreference = "Stop" + +$version = $Tag.TrimStart("v") +$installerName = "ControlFS-Setup-x64.exe" +$url = "https://github.com/$Repo/releases/download/$Tag/$installerName" + +# O hash é do arquivo que a release realmente publica (nunca de uma compilação local): baixa e calcula. +$work = Join-Path ([System.IO.Path]::GetTempPath()) ("controlfs-winget-" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Force -Path $work | Out-Null +try { + $download = Join-Path $work $installerName + Invoke-WebRequest -Uri $url -OutFile $download -UseBasicParsing + $sha = (Get-FileHash -Algorithm SHA256 -LiteralPath $download).Hash.ToUpperInvariant() +} +finally { + Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue +} + +$id = "nextestudios.ControlFS" +$schema = "1.9.0" +$date = (Get-Date).ToString("yyyy-MM-dd") +$dir = Join-Path $OutDir "manifests\n\nextestudios\ControlFS\$version" +New-Item -ItemType Directory -Force -Path $dir | Out-Null + +$header = "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.{0}.{1}.schema.json" + +@" +$($header -f "version", $schema) +PackageIdentifier: $id +PackageVersion: $version +DefaultLocale: en-US +ManifestType: version +ManifestVersion: $schema +"@ | Set-Content -Encoding utf8 (Join-Path $dir "$id.yaml") + +@" +$($header -f "installer", $schema) +PackageIdentifier: $id +PackageVersion: $version +InstallerType: inno +Scope: user +InstallModes: +- interactive +- silent +- silentWithProgress +UpgradeBehavior: install +MinimumOSVersion: 10.0.19041.0 +ReleaseDate: $date +Installers: +- Architecture: x64 + InstallerUrl: $url + InstallerSha256: $sha +ManifestType: installer +ManifestVersion: $schema +"@ | Set-Content -Encoding utf8 (Join-Path $dir "$id.installer.yaml") + +@" +$($header -f "defaultLocale", $schema) +PackageIdentifier: $id +PackageVersion: $version +PackageLocale: en-US +Publisher: nextestudios +PublisherUrl: https://github.com/$Repo +PublisherSupportUrl: https://github.com/$Repo/issues +PackageName: ControlFS +PackageUrl: https://github.com/$Repo +License: AGPL-3.0-only +LicenseUrl: https://github.com/$Repo/blob/main/LICENSE +ShortDescription: Controller-first file manager for Windows. +Description: ControlFS is a native Windows file manager you can drive entirely with a game controller (or keyboard and mouse), with tabs, dual pane, archives, previews and a built-in media player. +Moniker: controlfs +Tags: +- file-manager +- gamepad +- controller +- explorer +- archive +ReleaseNotesUrl: https://github.com/$Repo/releases/tag/$Tag +ManifestType: defaultLocale +ManifestVersion: $schema +"@ | Set-Content -Encoding utf8 (Join-Path $dir "$id.locale.en-US.yaml") + +Write-Host "Manifestos em $dir (SHA-256 do instalador: $sha)" + +if (Get-Command winget -ErrorAction SilentlyContinue) { + winget validate --manifest $dir + if ($LASTEXITCODE -ne 0) { throw "winget validate falhou ($LASTEXITCODE)" } +} else { + Write-Warning "winget não encontrado: manifestos gerados, mas não validados." +} diff --git a/docs/build-and-release.md b/docs/build-and-release.md index c936851c..5c32aedc 100644 --- a/docs/build-and-release.md +++ b/docs/build-and-release.md @@ -112,3 +112,13 @@ Na próxima release, confira no log "Release será assinada (provedor: …)" e a atualize `docs/CODE_SIGNING.md` (que ainda diz "ainda não têm assinatura"). O desinstalador do Inno Setup (`unins000.exe`) não é assinado nesta etapa. + +## WinGet (#273) + +O pacote do WinGet é `nextestudios.ControlFS` (instalador Inno por usuário, x64, sem administrador). O manifesto **sempre** vem de uma release já publicada: nunca se envia um pacote antes de o instalador oficial existir na URL da release. + +- **Gerar e validar:** Actions → **WinGet manifest** → Run workflow com a tag (ex.: `v0.12.0-alpha.1`). O workflow baixa o `ControlFS-Setup-x64.exe` da release, calcula o SHA-256, gera os três manifestos (`version`, `installer`, `defaultLocale`) com `build/New-WingetManifest.ps1`, roda `winget validate` e guarda o resultado no artefato `winget-manifests`. Não envia nada para fora. Localmente (Windows): `.\build\New-WingetManifest.ps1 -Tag v0.12.0-alpha.1`. +- **Enviar (passo manual, por quem é dono do pacote):** copiar a pasta `manifests\n\nextestudios\ControlFS\` do artefato para um fork de `microsoft/winget-pkgs` e abrir o PR (ou `wingetcreate submit`), respondendo à revisão. Só depois de aceito o pacote aparece no catálogo. +- **A cada release estável nova:** repetir os dois passos acima com a nova tag. O `AppId` do instalador nunca muda, então `winget upgrade` atualiza a mesma instalação sem mexer nas preferências (ficam em `%LOCALAPPDATA%\ControlFS`). +- **Depois de aceito:** documentar `winget install nextestudios.ControlFS` e `winget upgrade nextestudios.ControlFS` no README. Instalação direta, portátil e atualização dentro do app continuam como estão. +- **Sem assinatura (#84):** o instalador ainda não é assinado; o WinGet aceita, mas o SmartScreen pode avisar. A aprovação e a primeira verificação ficam melhores depois da assinatura.