From eba72eca86943f9dac60d61482a47b777e19215c Mon Sep 17 00:00:00 2001 From: leoars <6502186+leoars@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:56:19 -0300 Subject: [PATCH 1/2] [Claude] Adiciona workflow de release por CI e documenta o aviso do SmartScreen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yml: validação tag/versão/marco, publish self-contained, verificação de pacote, Inno Setup 7.1.1 fixado por hash, ZIP e instalador, jobs de assinatura SignPath reservados, attestation de proveniência e pré-release somente em rascunho. Build-GraphicalInstaller.ps1 ganha -SkipFrozenManifest para o payload gerado no próprio run. Seção SmartScreen nos READMEs e em getting-started; processo de release em development. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 398 +++++++++++++++++++++++++++ README.md | 10 + README.pt-BR.md | 10 + docs/en/development.md | 10 + docs/en/getting-started.md | 10 + docs/en/graphical-installer.md | 2 +- docs/pt-BR/development.md | 10 + docs/pt-BR/getting-started.md | 10 + docs/pt-BR/graphical-installer.md | 2 +- scripts/Build-GraphicalInstaller.ps1 | 10 +- 10 files changed, 467 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0e23b35 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,398 @@ +# Release: build the portable package and the graphical installer on a GitHub-hosted +# Windows runner from a version tag, attest their provenance and open a DRAFT +# pre-release. Nothing is published automatically and existing releases are never +# touched. Code signing jobs are reserved for the SignPath Foundation program and +# stay skipped until the repository variable SIGNPATH_ENABLED is 'true'. +# +# Triggers: a pushed tag `v*`, or workflow_dispatch with the tag name. A dispatch tag +# may carry semver build metadata (`v0.1.0-alpha.1+ci.7`) to produce a test draft that +# does not collide with a real tag; the version before `+` must still match +# installer/release.json. Ordinary pushes to main never run this workflow. +name: Release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + tag: + description: 'Release tag, e.g. v0.1.0-alpha.2. Add +ci.N for a test draft.' + required: true + type: string + +permissions: {} + +concurrency: + group: release-${{ github.event.inputs.tag || github.ref_name }} + cancel-in-progress: false + +env: + DOTNET_NOLOGO: 'true' + DOTNET_CLI_TELEMETRY_OPTOUT: 'true' + # Deterministic, path-independent compilation so a rebuild of the same source is comparable. + ContinuousIntegrationBuild: 'true' + Deterministic: 'true' + INNO_SETUP_VERSION: '7.1.1' + INNO_SETUP_URL: 'https://github.com/jrsoftware/issrc/releases/download/is-7_1_1/innosetup-7.1.1-x64.exe' + INNO_SETUP_SHA256: '8A5A6711C3766A6E5FB74F280BA0E3551E719CE94538D901FDBD1F8F9920B3F0' + +jobs: + validate: + name: Validate tag and declared version + runs-on: windows-latest + timeout-minutes: 10 + permissions: + contents: read + outputs: + tag: ${{ steps.check.outputs.tag }} + version: ${{ steps.check.outputs.version }} + milestone: ${{ steps.check.outputs.milestone }} + test_draft: ${{ steps.check.outputs.test_draft }} + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Compare tag with installer/release.json, BuildInfo.cs and Directory.Build.props + id: check + shell: pwsh + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + REF_NAME: ${{ github.ref_name }} + run: | + $ErrorActionPreference = 'Stop' + $tag = if ($env:EVENT_NAME -eq 'workflow_dispatch') { $env:INPUT_TAG.Trim() } else { $env:REF_NAME } + if ($tag -cnotmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)(\+(?[0-9A-Za-z.-]+))?$') { throw "Tag '$tag' is not a semver release tag (vMAJOR.MINOR.PATCH[-prerelease][+metadata])." } + $version = $Matches['version']; $meta = $Matches['meta'] + if ($meta -and $env:EVENT_NAME -ne 'workflow_dispatch') { throw 'Build metadata in a pushed tag is not allowed; use workflow_dispatch for test drafts.' } + $release = Get-Content -LiteralPath 'installer/release.json' -Raw | ConvertFrom-Json + $buildInfo = Get-Content -LiteralPath 'src/PGMS.Desktop/BuildInfo.cs' -Raw + if ($buildInfo -notmatch 'Milestone = "(M[0-9]+)"') { throw 'BuildInfo.cs milestone missing.' } + $milestone = $Matches[1] + [xml]$props = Get-Content -LiteralPath 'Directory.Build.props' -Raw + $propsVersion = ($props.Project.PropertyGroup | ForEach-Object { $_.Version } | Where-Object { $_ } | Select-Object -First 1) + $problems = @() + if ($version -cne $release.Version) { $problems += "tag version '$version' differs from installer/release.json Version '$($release.Version)'" } + if ($propsVersion -cne $release.Version) { $problems += "Directory.Build.props Version '$propsVersion' differs from installer/release.json Version '$($release.Version)'" } + if ($milestone -cne $release.Milestone) { $problems += "BuildInfo.cs milestone '$milestone' differs from installer/release.json Milestone '$($release.Milestone)'" } + if ($problems) { throw ("Version declaration mismatch: " + ($problems -join '; ')) } + "tag=$tag" >> $env:GITHUB_OUTPUT + "version=$version" >> $env:GITHUB_OUTPUT + "milestone=$milestone" >> $env:GITHUB_OUTPUT + "test_draft=$([string]::IsNullOrEmpty($meta) ? 'false' : 'true')" >> $env:GITHUB_OUTPUT + "Tag $tag matches version $version, milestone $milestone (test draft: $([bool]$meta))." >> $env:GITHUB_STEP_SUMMARY + + build: + name: Build self-contained payload + needs: validate + runs-on: windows-latest + timeout-minutes: 45 + permissions: + contents: read + outputs: + artifact-id: ${{ steps.upload.outputs.artifact-id }} + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up .NET SDK from global.json + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + + - name: Publish portable package (Release, locked restore, ReadyToRun) + shell: pwsh + run: ./scripts/Publish.ps1 -SelfContained + + - name: Verify manifest, package fixtures and bundled guides + shell: pwsh + env: + MILESTONE: ${{ needs.validate.outputs.milestone }} + run: | + $ErrorActionPreference = 'Stop' + $package = "artifacts/$env:MILESTONE-win-x64" + ./scripts/Verify-Package.ps1 -PackagePath $package | Format-List | Out-String | Write-Host + ./scripts/Test-Package.ps1 + ./scripts/Test-PackageGuides.ps1 -PackagePath $package | Format-List | Out-String | Write-Host + "Payload manifest SHA256: $((Get-FileHash "$package/package-manifest.json" -Algorithm SHA256).Hash)" >> $env:GITHUB_STEP_SUMMARY + + - name: Upload unsigned payload + id: upload + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: payload-unsigned + path: artifacts/${{ needs.validate.outputs.milestone }}-win-x64 + if-no-files-found: error + retention-days: 7 + + # Reserved for SignPath Foundation: signs PGMS.exe and the project's own DLLs before + # they enter the ZIP and the installer. Skipped until SIGNPATH_ENABLED is 'true'. + sign-binaries: + name: Sign application binaries (SignPath) + needs: [validate, build] + if: vars.SIGNPATH_ENABLED == 'true' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Submit payload to SignPath + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }} + github-artifact-id: ${{ needs.build.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: signed-payload + + - name: Upload signed payload + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: payload-signed + path: signed-payload + if-no-files-found: error + retention-days: 7 + + package: + name: Package ZIP and graphical installer + needs: [validate, build, sign-binaries] + if: always() && needs.build.result == 'success' && (needs.sign-binaries.result == 'success' || needs.sign-binaries.result == 'skipped') + runs-on: windows-latest + timeout-minutes: 45 + permissions: + contents: read + outputs: + installer-artifact-id: ${{ steps.upload-installer.outputs.artifact-id }} + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Download payload + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: ${{ needs.sign-binaries.result == 'success' && 'payload-signed' || 'payload-unsigned' }} + path: artifacts/${{ needs.validate.outputs.milestone }}-win-x64 + + - name: Refresh manifest after signing + if: needs.sign-binaries.result == 'success' + shell: pwsh + env: + MILESTONE: ${{ needs.validate.outputs.milestone }} + run: | + $ErrorActionPreference = 'Stop' + . ./scripts/PackageTools.ps1 + # Signatures change the hashes of the signed files; the manifest must describe the final bytes. + Remove-Item -LiteralPath "artifacts/$env:MILESTONE-win-x64/package-manifest.json" + New-PgmsPackageManifest "artifacts/$env:MILESTONE-win-x64" $env:MILESTONE $true | Out-Null + + - name: Verify payload and create ZIP with checksum + shell: pwsh + env: + MILESTONE: ${{ needs.validate.outputs.milestone }} + run: | + $ErrorActionPreference = 'Stop' + $package = "artifacts/$env:MILESTONE-win-x64" + ./scripts/Verify-Package.ps1 -PackagePath $package | Format-List | Out-String | Write-Host + ./scripts/New-PackageArchive.ps1 -PackagePath $package | Format-List | Out-String | Write-Host + + - name: Install Inno Setup ${{ env.INNO_SETUP_VERSION }} (pinned, hash-verified, portable) + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $download = Join-Path $env:RUNNER_TEMP 'innosetup-download' + New-Item -ItemType Directory -Path $download -Force | Out-Null + $setup = Join-Path $download 'innosetup-x64.exe' + Invoke-WebRequest -Uri $env:INNO_SETUP_URL -OutFile $setup + $hash = (Get-FileHash -LiteralPath $setup -Algorithm SHA256).Hash + if ($hash -cne $env:INNO_SETUP_SHA256) { throw "Inno Setup download hash $hash does not match the pinned $env:INNO_SETUP_SHA256." } + $tools = Join-Path $env:RUNNER_TEMP 'inno-setup' + $process = Start-Process -FilePath $setup -ArgumentList @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/PORTABLE=1', "/DIR=`"$tools`"") -PassThru -Wait + if ($process.ExitCode -ne 0) { throw "Inno Setup installer exited with $($process.ExitCode)." } + $compiler = Join-Path $tools 'ISCC.exe' + if (!(Test-Path -LiteralPath $compiler)) { throw 'ISCC.exe not found after portable installation.' } + "INNO_SETUP_COMPILER=$compiler" >> $env:GITHUB_ENV + + - name: Build graphical installer from the CI payload + shell: pwsh + env: + MILESTONE: ${{ needs.validate.outputs.milestone }} + VERSION: ${{ needs.validate.outputs.version }} + run: | + $ErrorActionPreference = 'Stop' + # The frozen manifest hash in installer/release.json belongs to the payload that was built locally; + # here the payload comes from this very run, so only milestone and product version are compared. + ./scripts/Build-GraphicalInstaller.ps1 -PackagePath "artifacts/$env:MILESTONE-win-x64" -CompilerPath $env:INNO_SETUP_COMPILER -OutputDirectory "artifacts/graphical-installer/$env:VERSION" -SkipFrozenManifest | Format-List | Out-String | Write-Host + + - name: Smoke-open the packaged application (informational) + id: smoke + continue-on-error: true + shell: pwsh + env: + MILESTONE: ${{ needs.validate.outputs.milestone }} + run: ./scripts/Test-PackageLaunch.ps1 -PackagePath "artifacts/$env:MILESTONE-win-x64" | Format-List | Out-String | Write-Host + + - name: Record smoke outcome + shell: pwsh + env: + SMOKE: ${{ steps.smoke.outcome }} + run: '"Launch smoke on the hosted runner: $env:SMOKE (informational; the runner session is not a certified desktop)." >> $env:GITHUB_STEP_SUMMARY' + + - name: Collect release assets + shell: pwsh + env: + VERSION: ${{ needs.validate.outputs.version }} + MILESTONE: ${{ needs.validate.outputs.milestone }} + run: | + $ErrorActionPreference = 'Stop' + New-Item -ItemType Directory -Path release-assets | Out-Null + Copy-Item -LiteralPath "artifacts/distribution/PGMS-$env:MILESTONE-win-x64.zip" -Destination "release-assets/PGMS-$env:VERSION-win-x64.zip" + Copy-Item -LiteralPath "artifacts/$env:MILESTONE-win-x64/package-manifest.json" -Destination "release-assets/package-manifest.json" + Get-ChildItem release-assets | Format-Table Name, Length | Out-String | Write-Host + + - name: Upload ZIP and manifest + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: release-zip + path: release-assets + if-no-files-found: error + retention-days: 7 + + - name: Upload unsigned installer + id: upload-installer + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: installer-unsigned + path: artifacts/graphical-installer/${{ needs.validate.outputs.version }}/PGMS-${{ needs.validate.outputs.version }}-Setup-x64.exe + if-no-files-found: error + retention-days: 7 + + # Reserved for SignPath Foundation: signs the installer EXE last, after the signed + # application binaries were packaged. Skipped until SIGNPATH_ENABLED is 'true'. + sign-installer: + name: Sign installer (SignPath) + needs: [validate, package] + if: always() && needs.package.result == 'success' && vars.SIGNPATH_ENABLED == 'true' + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Submit installer to SignPath + uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }} + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG }} + github-artifact-id: ${{ needs.package.outputs.installer-artifact-id }} + wait-for-completion: true + output-artifact-directory: signed-installer + + - name: Upload signed installer + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: installer-signed + path: signed-installer + if-no-files-found: error + retention-days: 7 + + attest: + name: Attest build provenance + needs: [validate, package, sign-installer] + if: always() && needs.package.result == 'success' && (needs.sign-installer.result == 'success' || needs.sign-installer.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + id-token: write + attestations: write + contents: read + steps: + - name: Download ZIP + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-zip + path: final + + - name: Download installer + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: ${{ needs.sign-installer.result == 'success' && 'installer-signed' || 'installer-unsigned' }} + path: final + + - name: Attest ZIP and installer + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: | + final/*.zip + final/*.exe + + release: + name: Create draft pre-release + needs: [validate, package, sign-installer, attest] + if: always() && needs.package.result == 'success' && needs.attest.result == 'success' && (needs.sign-installer.result == 'success' || needs.sign-installer.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + steps: + - name: Check out (release notes) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Download ZIP + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: release-zip + path: final + + - name: Download installer + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: ${{ needs.sign-installer.result == 'success' && 'installer-signed' || 'installer-unsigned' }} + path: final + + - name: Assemble checksums and notes + shell: bash + env: + TAG: ${{ needs.validate.outputs.tag }} + VERSION: ${{ needs.validate.outputs.version }} + TEST_DRAFT: ${{ needs.validate.outputs.test_draft }} + SIGNED: ${{ needs.sign-installer.result == 'success' }} + run: | + set -euo pipefail + cp docs/en/release-notes.md final/RELEASE-NOTES.md + cp docs/pt-BR/release-notes.md final/RELEASE-NOTES.pt-BR.md + (cd final && sha256sum PGMS-*.zip PGMS-*.exe RELEASE-NOTES.md RELEASE-NOTES.pt-BR.md package-manifest.json > SHA256SUMS.txt) + cat final/SHA256SUMS.txt + { + echo "Draft generated by GitHub Actions run ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} from commit ${GITHUB_SHA}." + if [ "$TEST_DRAFT" = "true" ]; then echo; echo "**Test draft (tag carries build metadata).** Delete after review; do not publish."; fi + echo + if [ "$SIGNED" = "true" ]; then echo "Binaries and installer signed through SignPath. Free code signing provided by SignPath.io, certificate by SignPath Foundation."; else echo "Binaries and installer are not code-signed; Windows SmartScreen may warn. Verify \`SHA256SUMS.txt\` and the build provenance attestation (\`gh attestation verify --repo ${GITHUB_REPOSITORY}\`)."; fi + echo + echo "Release notes: see the attached RELEASE-NOTES.md (English) and RELEASE-NOTES.pt-BR.md (Português do Brasil). Review them before publishing; they must describe version ${VERSION}." + } > body.md + cat body.md + + - name: Create DRAFT pre-release (never published automatically) + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.validate.outputs.tag }} + VERSION: ${{ needs.validate.outputs.version }} + TEST_DRAFT: ${{ needs.validate.outputs.test_draft }} + run: | + set -euo pipefail + if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + echo "A release for tag $TAG already exists; it will not be modified." >&2 + exit 1 + fi + title="PGMS $VERSION" + if [ "$TEST_DRAFT" = "true" ]; then title="$title (CI test draft, do not publish)"; fi + gh release create "$TAG" --repo "$GITHUB_REPOSITORY" --draft --prerelease --target "$GITHUB_SHA" --title "$title" --notes-file body.md \ + final/PGMS-*.zip final/PGMS-*.exe final/SHA256SUMS.txt final/RELEASE-NOTES.md final/RELEASE-NOTES.pt-BR.md final/package-manifest.json + url=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json url --jq .url) + echo "Draft pre-release created: $url" >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index 842bd2d..19a932e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,16 @@ English | [Português (Brasil)](README.pt-BR.md) Download the installer or the ZIP from the [latest release](https://github.com/leoars/PostgresManagementStudio/releases), run it, then use **Connect** and **New Query**. Backup/restore needs the PostgreSQL client tools (`pg_dump`, `pg_restore`, `psql`) of the same major version as the server. Details: [setup guide](docs/en/getting-started.md). +#### Windows SmartScreen warning + +PGMS releases are not code-signed yet, so Windows may show "Windows protected your PC" the first time you run the installer or `PGMS.exe`. Choose **More info** and then **Run anyway**. Before doing so, confirm the download matches the checksums published with the release: + +```powershell +Get-FileHash .\PGMS-* -Algorithm SHA256 +``` + +Compare the hashes with `SHA256SUMS.txt` and the installer's `.sha256` file in the release. v0.1.0-alpha.1 was built on the maintainer's machine, so it has checksums but no build provenance attestation; releases produced by the [release workflow](.github/workflows/release.yml) can additionally be verified with `gh attestation verify`. Code signing through the SignPath Foundation open source program is planned. + ### Build from source For development: Windows x64, PowerShell 7 and the .NET SDK selected by [global.json](global.json) (`10.0.400`, latest installed patch in that feature band). PostgreSQL 17/18 and matching-major tools are needed for database tests and backup/restore. diff --git a/README.pt-BR.md b/README.pt-BR.md index 5fdfdec..1a9de8f 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -35,6 +35,16 @@ Baixe o instalador ou o ZIP na [release mais recente](https://github.com/leoars/PostgresManagementStudio/releases), execute e use **Connect** e **New Query**. Backup/restore exige as ferramentas cliente do PostgreSQL (`pg_dump`, `pg_restore`, `psql`) da mesma versão principal do servidor. Detalhes: [guia de preparação](docs/pt-BR/getting-started.md). +#### Aviso do SmartScreen do Windows + +As releases do PGMS ainda não são assinadas digitalmente, então o Windows pode mostrar "O Windows protegeu o computador" na primeira execução do instalador ou do `PGMS.exe`. Clique em **Mais informações** e depois em **Executar assim mesmo**. Antes disso, confirme que o download corresponde aos checksums publicados com a release: + +```powershell +Get-FileHash .\PGMS-* -Algorithm SHA256 +``` + +Compare os hashes com o `SHA256SUMS.txt` e com o arquivo `.sha256` do instalador na release. A v0.1.0-alpha.1 foi compilada na máquina do mantenedor, então tem checksums mas não tem atestado de proveniência de build; releases geradas pelo [workflow de release](.github/workflows/release.yml) podem ser verificadas também com `gh attestation verify`. A assinatura de código pelo programa open source da SignPath Foundation está planejada. + ### Compilar a partir do código Para desenvolvimento: Windows x64, PowerShell 7 e SDK .NET selecionado por [global.json](global.json) (`10.0.400`, com o patch mais recente instalado nessa faixa). PostgreSQL 17/18 e ferramentas da mesma versão principal são necessários para testes com banco e backup/restore. diff --git a/docs/en/development.md b/docs/en/development.md index bf9c369..e939e1c 100644 --- a/docs/en/development.md +++ b/docs/en/development.md @@ -33,6 +33,16 @@ Choose only one focused suite per invocation. Some fixtures intentionally create When publishing a release, upload one `SHA256SUMS.txt` that covers every binary asset (ZIP, PowerShell installer and graphical installer EXE) instead of per-asset checksum files, and do not replace assets or checksums after publication; publish a new tag instead. +## Release + +Release binaries come from [release.yml](../../.github/workflows/release.yml) on a GitHub-hosted Windows runner, not from a developer machine. Before tagging, declare the same version in `Directory.Build.props`, `installer/release.json` (`Version`, `NumericVersion`, `Milestone`) and `src/PGMS.Desktop/BuildInfo.cs`; the workflow fails when the tag (`v` plus the version) and these files disagree. Pushing a tag `v*`, or running the workflow manually with the tag name, publishes the ReadyToRun payload with `Publish.ps1 -SelfContained` under deterministic build settings, runs `Verify-Package.ps1`, `Test-Package.ps1` and `Test-PackageGuides.ps1`, creates the ZIP with `New-PackageArchive.ps1`, installs the pinned, hash-verified Inno Setup 7.1.1 in portable mode and builds the installer with `Build-GraphicalInstaller.ps1 -SkipFrozenManifest` (the frozen manifest hash in `release.json` describes a payload built locally; in CI the payload comes from the run itself, so only milestone and product version are compared). It then writes `SHA256SUMS.txt`, attests build provenance for the ZIP and the installer and creates a **draft** pre-release with the notes from `docs/en` and `docs/pt-BR`. Publishing the draft, and any change to an existing release, remains a manual decision; a release that already exists for the tag makes the workflow fail. A manual run whose tag carries build metadata (`v0.1.0-alpha.1+ci.1`) produces a test draft that must be deleted after review. Verify an asset produced by the workflow with: + +```powershell +gh attestation verify .\ --repo leoars/PostgresManagementStudio +``` + +The jobs `sign-binaries` and `sign-installer` are reserved for the SignPath Foundation open source program and stay skipped until the repository variable `SIGNPATH_ENABLED` is `true`; they then require the secret `SIGNPATH_API_TOKEN` and the variables `SIGNPATH_ORGANIZATION_ID`, `SIGNPATH_PROJECT_SLUG`, `SIGNPATH_SIGNING_POLICY_SLUG` and `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG`. Application binaries are signed before they enter the ZIP and the installer, the installer is signed last, and checksums and attestations cover the final files. + ## Change workflow Read [CONTRIBUTING.md](../../CONTRIBUTING.md) and [AGENTS.md](../../AGENTS.md). Keep public English/pt-BR guides synchronized. Report validation actually run and its limitations. Record relevant changes and validation in the pull request. GitHub Actions ([build.yml](../../.github/workflows/build.yml)) runs `Build.ps1 -Configuration Release` and `Test-PublicDocumentation.ps1` on Windows for pushes and pull requests; database and WPF suites are not run there and must be reported manually. Dependabot opens weekly pull requests for NuGet packages and pinned actions. Never commit profiles, tokens, private keys, dumps, build outputs or local acceptance artifacts. Gitleaks can scan history with `gitleaks git . --log-opts="--all --full-history" --redact=100`. This source repository starts with a new history and no historical secret-scan exceptions. diff --git a/docs/en/getting-started.md b/docs/en/getting-started.md index c463798..c86cd94 100644 --- a/docs/en/getting-started.md +++ b/docs/en/getting-started.md @@ -4,6 +4,16 @@ English | [Português (Brasil)](../pt-BR/getting-started.md) [Download v0.1.0-alpha.1](https://github.com/leoars/PostgresManagementStudio/releases/tag/v0.1.0-alpha.1): [graphical installer](graphical-installer.md), portable ZIP or PowerShell 7 installer. The graphical installer offers current-user and all-users modes and does not require PowerShell. See [release notes](release-notes.md). +### Windows SmartScreen warning + +PGMS releases are not code-signed yet, so Windows may show "Windows protected your PC" the first time you run the installer or `PGMS.exe`. Choose **More info** and then **Run anyway**. Before doing so, confirm the download matches the checksums published with the release: + +```powershell +Get-FileHash .\PGMS-* -Algorithm SHA256 +``` + +Compare the hashes with `SHA256SUMS.txt` and the installer's `.sha256` file in the release. v0.1.0-alpha.1 was built on the maintainer's machine, so it has checksums but no build provenance attestation; releases produced by the [release workflow](../../.github/workflows/release.yml) can additionally be verified with `gh attestation verify` (see [development](development.md)). Code signing through the SignPath Foundation open source program is planned. + ## Requirements PGMS targets Windows x64. Building requires PowerShell 7 and a .NET 10 SDK compatible with [global.json](../../global.json): 10.0.400 with latest-patch roll-forward. Dependencies are pinned in NuGet lockfiles. The M1008 package was built with SDK 10.0.403 and bundles .NET/WPF 10.0.14; a rebuild may use different installed runtime packs. diff --git a/docs/en/graphical-installer.md b/docs/en/graphical-installer.md index 8b528ef..4fc6f4c 100644 --- a/docs/en/graphical-installer.md +++ b/docs/en/graphical-installer.md @@ -23,7 +23,7 @@ The executable is unsigned; SmartScreen reputation and a clean Windows VM have n ## Build and test -Use the official [Inno Setup 7.1.1 x64 compiler](https://jrsoftware.org/isdl.php), retaining its license. Extract the existing release ZIP into `artifacts/M1008-win-x64`. The builder checks every payload file, the frozen manifest and application version, and refuses an existing output directory. A local application rebuild may have a different manifest and is intentionally refused by this release-specific builder. Update the release configuration for a future frozen release. +Use the official [Inno Setup 7.1.1 x64 compiler](https://jrsoftware.org/isdl.php), retaining its license. Extract the existing release ZIP into `artifacts/M1008-win-x64`. The builder checks every payload file, the frozen manifest and application version, and refuses an existing output directory. A local application rebuild may have a different manifest and is intentionally refused by this release-specific builder. Update the release configuration for a future frozen release. The [release workflow](../../.github/workflows/release.yml) passes `-SkipFrozenManifest` because it builds the payload itself; milestone and product version are still enforced, and the frozen hash remains required for local builds. ```powershell ./scripts/Build-GraphicalInstaller.ps1 -PackagePath artifacts/M1008-win-x64 -CompilerPath 'C:\Tools\Inno Setup 7\ISCC.exe' diff --git a/docs/pt-BR/development.md b/docs/pt-BR/development.md index a578ebc..d051337 100644 --- a/docs/pt-BR/development.md +++ b/docs/pt-BR/development.md @@ -33,6 +33,16 @@ Escolha apenas uma suíte focada por execução. Algumas fixtures criam junction Ao publicar uma release, envie um único `SHA256SUMS.txt` cobrindo todos os assets binários (ZIP, instalador PowerShell e EXE do instalador gráfico) em vez de arquivos de checksum por asset, e não substitua assets ou checksums após a publicação; publique uma nova tag. +## Release + +Os binários de release saem do [release.yml](../../.github/workflows/release.yml) em um runner Windows hospedado pelo GitHub, não de uma máquina de desenvolvedor. Antes de criar a tag, declare a mesma versão em `Directory.Build.props`, `installer/release.json` (`Version`, `NumericVersion`, `Milestone`) e `src/PGMS.Desktop/BuildInfo.cs`; o workflow falha quando a tag (`v` mais a versão) e esses arquivos divergem. Um push de tag `v*`, ou a execução manual do workflow com o nome da tag, publica o payload ReadyToRun com `Publish.ps1 -SelfContained` em build determinístico, roda `Verify-Package.ps1`, `Test-Package.ps1` e `Test-PackageGuides.ps1`, cria o ZIP com `New-PackageArchive.ps1`, instala o Inno Setup 7.1.1 fixado e conferido por hash em modo portátil e compila o instalador com `Build-GraphicalInstaller.ps1 -SkipFrozenManifest` (o hash de manifesto congelado em `release.json` descreve um payload compilado localmente; no CI o payload vem da própria execução, então só marco e versão do produto são comparados). Em seguida grava `SHA256SUMS.txt`, atesta a proveniência do ZIP e do instalador e cria uma pré-release em **rascunho** com as notas de `docs/en` e `docs/pt-BR`. Publicar o rascunho, e qualquer alteração em release existente, continua decisão manual; uma release já existente para a tag faz o workflow falhar. Uma execução manual cuja tag traga metadados de build (`v0.1.0-alpha.1+ci.1`) gera um rascunho de teste que deve ser apagado após a revisão. Verifique um asset gerado pelo workflow com: + +```powershell +gh attestation verify .\ --repo leoars/PostgresManagementStudio +``` + +Os jobs `sign-binaries` e `sign-installer` estão reservados ao programa open source da SignPath Foundation e permanecem pulados até a variável de repositório `SIGNPATH_ENABLED` valer `true`; então exigem o secret `SIGNPATH_API_TOKEN` e as variáveis `SIGNPATH_ORGANIZATION_ID`, `SIGNPATH_PROJECT_SLUG`, `SIGNPATH_SIGNING_POLICY_SLUG` e `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG`. Os binários do aplicativo são assinados antes de entrar no ZIP e no instalador, o instalador é assinado por último, e checksums e atestados cobrem os arquivos finais. + ## Fluxo de alteração Leia [CONTRIBUTING.pt-BR.md](../../CONTRIBUTING.pt-BR.md) e [AGENTS.md](../../AGENTS.md). Sincronize guias públicos inglês/pt-BR. Informe validação realmente executada e limitações. Registre mudanças relevantes e validação no pull request. O GitHub Actions ([build.yml](../../.github/workflows/build.yml)) executa `Build.ps1 -Configuration Release` e `Test-PublicDocumentation.ps1` no Windows em pushes e pull requests; as suítes de banco e WPF não rodam lá e devem ser relatadas manualmente. O Dependabot abre pull requests semanais para pacotes NuGet e actions fixadas. Nunca versione perfis, tokens, chaves privadas, dumps, saídas de build ou artefatos locais de aceitação. Gitleaks examina o histórico com `gitleaks git . --log-opts="--all --full-history" --redact=100`. Este repositório de fonte começa com histórico novo e sem exceções históricas da varredura. diff --git a/docs/pt-BR/getting-started.md b/docs/pt-BR/getting-started.md index 8dcafac..44af3b7 100644 --- a/docs/pt-BR/getting-started.md +++ b/docs/pt-BR/getting-started.md @@ -4,6 +4,16 @@ [Baixar v0.1.0-alpha.1](https://github.com/leoars/PostgresManagementStudio/releases/tag/v0.1.0-alpha.1): [instalador gráfico](graphical-installer.md), ZIP portátil ou instalador PowerShell 7. O instalador gráfico oferece modos por usuário e para todos, dispensando PowerShell. Veja [notas da versão](release-notes.md). +### Aviso do SmartScreen do Windows + +As releases do PGMS ainda não são assinadas digitalmente, então o Windows pode mostrar "O Windows protegeu o computador" na primeira execução do instalador ou do `PGMS.exe`. Clique em **Mais informações** e depois em **Executar assim mesmo**. Antes disso, confirme que o download corresponde aos checksums publicados com a release: + +```powershell +Get-FileHash .\PGMS-* -Algorithm SHA256 +``` + +Compare os hashes com o `SHA256SUMS.txt` e com o arquivo `.sha256` do instalador na release. A v0.1.0-alpha.1 foi compilada na máquina do mantenedor, então tem checksums mas não tem atestado de proveniência de build; releases geradas pelo [workflow de release](../../.github/workflows/release.yml) podem ser verificadas também com `gh attestation verify` (veja [desenvolvimento](development.md)). A assinatura de código pelo programa open source da SignPath Foundation está planejada. + ## Pré-requisitos O PGMS é destinado a Windows x64. Compilar exige PowerShell 7 e SDK.NET10 compatível com [global.json](../../global.json): 10.0.400 com roll-forward para o patch mais recente. Dependências são fixadas nos lockfiles NuGet. O pacote M1008 foi compilado com SDK 10.0.403 e inclui .NET/WPF 10.0.14; recompilar pode usar outros runtime packs instalados. diff --git a/docs/pt-BR/graphical-installer.md b/docs/pt-BR/graphical-installer.md index 6f1101f..33dd396 100644 --- a/docs/pt-BR/graphical-installer.md +++ b/docs/pt-BR/graphical-installer.md @@ -23,7 +23,7 @@ O executável não tem assinatura digital; reputação SmartScreen e VM Windows ## Compilar e testar -Use o [compilador oficial Inno Setup 7.1.1 x64](https://jrsoftware.org/isdl.php), preservando sua licença. Extraia o ZIP existente da release em `artifacts/M1008-win-x64`. O builder confere cada arquivo, o manifesto congelado e a versão do aplicativo, recusando diretório de saída existente. Uma recompilação local pode produzir outro manifesto e é intencionalmente recusada por este builder específico da release. Atualize a configuração para uma futura release congelada. +Use o [compilador oficial Inno Setup 7.1.1 x64](https://jrsoftware.org/isdl.php), preservando sua licença. Extraia o ZIP existente da release em `artifacts/M1008-win-x64`. O builder confere cada arquivo, o manifesto congelado e a versão do aplicativo, recusando diretório de saída existente. Uma recompilação local pode produzir outro manifesto e é intencionalmente recusada por este builder específico da release. Atualize a configuração para uma futura release congelada. O [workflow de release](../../.github/workflows/release.yml) passa `-SkipFrozenManifest` porque compila o próprio payload; marco e versão do produto continuam exigidos, e o hash congelado segue obrigatório em builds locais. ```powershell ./scripts/Build-GraphicalInstaller.ps1 -PackagePath artifacts/M1008-win-x64 -CompilerPath 'C:\Tools\Inno Setup 7\ISCC.exe' diff --git a/scripts/Build-GraphicalInstaller.ps1 b/scripts/Build-GraphicalInstaller.ps1 index 16605dc..0bd7390 100644 --- a/scripts/Build-GraphicalInstaller.ps1 +++ b/scripts/Build-GraphicalInstaller.ps1 @@ -1,7 +1,10 @@ param( [Parameter(Mandatory)][string]$PackagePath, [Parameter(Mandatory)][string]$CompilerPath, - [string]$OutputDirectory + [string]$OutputDirectory, + # CI builds the payload from the tagged source, so its manifest hash is not known before the build; + # the switch skips only the frozen-hash comparison and keeps milestone and product version checks. + [switch]$SkipFrozenManifest ) $ErrorActionPreference = 'Stop' . (Join-Path $PSScriptRoot 'PackageTools.ps1') @@ -10,7 +13,8 @@ $release = Get-Content (Join-Path $root 'installer/release.json') -Raw | Convert $verified = Test-PgmsPackage $PackagePath $package = (Get-Item -LiteralPath $PackagePath).FullName if ($verified.Mode -ne 'SelfContained' -or $verified.Milestone -cne $release.Milestone) { throw 'Wrong release package.' } -if ((Get-FileHash (Join-Path $package 'package-manifest.json')).Hash -cne $release.ManifestSha256) { throw 'Release manifest does not match the frozen payload.' } +$manifestHash = (Get-FileHash (Join-Path $package 'package-manifest.json')).Hash +if (!$SkipFrozenManifest -and $manifestHash -cne $release.ManifestSha256) { throw 'Release manifest does not match the frozen payload.' } if ([Diagnostics.FileVersionInfo]::GetVersionInfo((Join-Path $package 'PGMS.dll')).ProductVersion -cne $release.Version) { throw 'Wrong product version.' } $compiler = (Get-Item -LiteralPath $CompilerPath).FullName if ((Get-AuthenticodeSignature -LiteralPath $compiler).Status -ne 'Valid') { throw 'Compiler signature is not valid.' } @@ -34,4 +38,4 @@ if ($LASTEXITCODE -ne 0) { throw "ISCC failed with exit code $LASTEXITCODE. Fail $exe = Join-Path $output "PGMS-$($release.Version)-Setup-x64.exe" $hash = (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash "$hash $([IO.Path]::GetFileName($exe))" | Set-Content -LiteralPath "$exe.sha256" -Encoding ascii -[pscustomobject]@{ Installer = $exe; Sha256 = $hash; CompilerVersion = $compilerVersion; PayloadManifest = $release.ManifestSha256 } +[pscustomobject]@{ Installer = $exe; Sha256 = $hash; CompilerVersion = $compilerVersion; PayloadManifest = $manifestHash; FrozenManifestChecked = !$SkipFrozenManifest } From d85d775c970bf9b782d11582f70a542eaa663304 Mon Sep 17 00:00:00 2001 From: leoars <6502186+leoars@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:57:33 -0300 Subject: [PATCH 2/2] [Claude] Permite tag de teste com metadados de build para gerar rascunho pelo workflow de release Co-Authored-By: Claude Fable 5.1 --- .github/workflows/release.yml | 10 +++++----- docs/en/development.md | 2 +- docs/pt-BR/development.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e23b35..0b26700 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,10 +4,11 @@ # touched. Code signing jobs are reserved for the SignPath Foundation program and # stay skipped until the repository variable SIGNPATH_ENABLED is 'true'. # -# Triggers: a pushed tag `v*`, or workflow_dispatch with the tag name. A dispatch tag -# may carry semver build metadata (`v0.1.0-alpha.1+ci.7`) to produce a test draft that -# does not collide with a real tag; the version before `+` must still match -# installer/release.json. Ordinary pushes to main never run this workflow. +# Triggers: a pushed tag `v*`, or workflow_dispatch with the tag name (dispatch only +# works once the workflow exists on the default branch). A tag that carries semver +# build metadata (`v0.1.0-alpha.1+ci.7`) produces a TEST draft that never collides with +# a real version tag; the version before `+` must still match installer/release.json. +# Ordinary pushes to main never run this workflow. name: Release on: @@ -64,7 +65,6 @@ jobs: $tag = if ($env:EVENT_NAME -eq 'workflow_dispatch') { $env:INPUT_TAG.Trim() } else { $env:REF_NAME } if ($tag -cnotmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)(\+(?[0-9A-Za-z.-]+))?$') { throw "Tag '$tag' is not a semver release tag (vMAJOR.MINOR.PATCH[-prerelease][+metadata])." } $version = $Matches['version']; $meta = $Matches['meta'] - if ($meta -and $env:EVENT_NAME -ne 'workflow_dispatch') { throw 'Build metadata in a pushed tag is not allowed; use workflow_dispatch for test drafts.' } $release = Get-Content -LiteralPath 'installer/release.json' -Raw | ConvertFrom-Json $buildInfo = Get-Content -LiteralPath 'src/PGMS.Desktop/BuildInfo.cs' -Raw if ($buildInfo -notmatch 'Milestone = "(M[0-9]+)"') { throw 'BuildInfo.cs milestone missing.' } diff --git a/docs/en/development.md b/docs/en/development.md index e939e1c..3b4994c 100644 --- a/docs/en/development.md +++ b/docs/en/development.md @@ -35,7 +35,7 @@ When publishing a release, upload one `SHA256SUMS.txt` that covers every binary ## Release -Release binaries come from [release.yml](../../.github/workflows/release.yml) on a GitHub-hosted Windows runner, not from a developer machine. Before tagging, declare the same version in `Directory.Build.props`, `installer/release.json` (`Version`, `NumericVersion`, `Milestone`) and `src/PGMS.Desktop/BuildInfo.cs`; the workflow fails when the tag (`v` plus the version) and these files disagree. Pushing a tag `v*`, or running the workflow manually with the tag name, publishes the ReadyToRun payload with `Publish.ps1 -SelfContained` under deterministic build settings, runs `Verify-Package.ps1`, `Test-Package.ps1` and `Test-PackageGuides.ps1`, creates the ZIP with `New-PackageArchive.ps1`, installs the pinned, hash-verified Inno Setup 7.1.1 in portable mode and builds the installer with `Build-GraphicalInstaller.ps1 -SkipFrozenManifest` (the frozen manifest hash in `release.json` describes a payload built locally; in CI the payload comes from the run itself, so only milestone and product version are compared). It then writes `SHA256SUMS.txt`, attests build provenance for the ZIP and the installer and creates a **draft** pre-release with the notes from `docs/en` and `docs/pt-BR`. Publishing the draft, and any change to an existing release, remains a manual decision; a release that already exists for the tag makes the workflow fail. A manual run whose tag carries build metadata (`v0.1.0-alpha.1+ci.1`) produces a test draft that must be deleted after review. Verify an asset produced by the workflow with: +Release binaries come from [release.yml](../../.github/workflows/release.yml) on a GitHub-hosted Windows runner, not from a developer machine. Before tagging, declare the same version in `Directory.Build.props`, `installer/release.json` (`Version`, `NumericVersion`, `Milestone`) and `src/PGMS.Desktop/BuildInfo.cs`; the workflow fails when the tag (`v` plus the version) and these files disagree. Pushing a tag `v*`, or running the workflow manually with the tag name, publishes the ReadyToRun payload with `Publish.ps1 -SelfContained` under deterministic build settings, runs `Verify-Package.ps1`, `Test-Package.ps1` and `Test-PackageGuides.ps1`, creates the ZIP with `New-PackageArchive.ps1`, installs the pinned, hash-verified Inno Setup 7.1.1 in portable mode and builds the installer with `Build-GraphicalInstaller.ps1 -SkipFrozenManifest` (the frozen manifest hash in `release.json` describes a payload built locally; in CI the payload comes from the run itself, so only milestone and product version are compared). It then writes `SHA256SUMS.txt`, attests build provenance for the ZIP and the installer and creates a **draft** pre-release with the notes from `docs/en` and `docs/pt-BR`. Publishing the draft, and any change to an existing release, remains a manual decision; a release that already exists for the tag makes the workflow fail. A tag or manual run whose name carries build metadata (`v0.1.0-alpha.1+ci.1`) produces a test draft that must be deleted after review, together with the test tag; manual runs are only possible after the workflow exists on `main`. Verify an asset produced by the workflow with: ```powershell gh attestation verify .\ --repo leoars/PostgresManagementStudio diff --git a/docs/pt-BR/development.md b/docs/pt-BR/development.md index d051337..2716ad9 100644 --- a/docs/pt-BR/development.md +++ b/docs/pt-BR/development.md @@ -35,7 +35,7 @@ Ao publicar uma release, envie um único `SHA256SUMS.txt` cobrindo todos os asse ## Release -Os binários de release saem do [release.yml](../../.github/workflows/release.yml) em um runner Windows hospedado pelo GitHub, não de uma máquina de desenvolvedor. Antes de criar a tag, declare a mesma versão em `Directory.Build.props`, `installer/release.json` (`Version`, `NumericVersion`, `Milestone`) e `src/PGMS.Desktop/BuildInfo.cs`; o workflow falha quando a tag (`v` mais a versão) e esses arquivos divergem. Um push de tag `v*`, ou a execução manual do workflow com o nome da tag, publica o payload ReadyToRun com `Publish.ps1 -SelfContained` em build determinístico, roda `Verify-Package.ps1`, `Test-Package.ps1` e `Test-PackageGuides.ps1`, cria o ZIP com `New-PackageArchive.ps1`, instala o Inno Setup 7.1.1 fixado e conferido por hash em modo portátil e compila o instalador com `Build-GraphicalInstaller.ps1 -SkipFrozenManifest` (o hash de manifesto congelado em `release.json` descreve um payload compilado localmente; no CI o payload vem da própria execução, então só marco e versão do produto são comparados). Em seguida grava `SHA256SUMS.txt`, atesta a proveniência do ZIP e do instalador e cria uma pré-release em **rascunho** com as notas de `docs/en` e `docs/pt-BR`. Publicar o rascunho, e qualquer alteração em release existente, continua decisão manual; uma release já existente para a tag faz o workflow falhar. Uma execução manual cuja tag traga metadados de build (`v0.1.0-alpha.1+ci.1`) gera um rascunho de teste que deve ser apagado após a revisão. Verifique um asset gerado pelo workflow com: +Os binários de release saem do [release.yml](../../.github/workflows/release.yml) em um runner Windows hospedado pelo GitHub, não de uma máquina de desenvolvedor. Antes de criar a tag, declare a mesma versão em `Directory.Build.props`, `installer/release.json` (`Version`, `NumericVersion`, `Milestone`) e `src/PGMS.Desktop/BuildInfo.cs`; o workflow falha quando a tag (`v` mais a versão) e esses arquivos divergem. Um push de tag `v*`, ou a execução manual do workflow com o nome da tag, publica o payload ReadyToRun com `Publish.ps1 -SelfContained` em build determinístico, roda `Verify-Package.ps1`, `Test-Package.ps1` e `Test-PackageGuides.ps1`, cria o ZIP com `New-PackageArchive.ps1`, instala o Inno Setup 7.1.1 fixado e conferido por hash em modo portátil e compila o instalador com `Build-GraphicalInstaller.ps1 -SkipFrozenManifest` (o hash de manifesto congelado em `release.json` descreve um payload compilado localmente; no CI o payload vem da própria execução, então só marco e versão do produto são comparados). Em seguida grava `SHA256SUMS.txt`, atesta a proveniência do ZIP e do instalador e cria uma pré-release em **rascunho** com as notas de `docs/en` e `docs/pt-BR`. Publicar o rascunho, e qualquer alteração em release existente, continua decisão manual; uma release já existente para a tag faz o workflow falhar. Uma tag ou execução manual cujo nome traga metadados de build (`v0.1.0-alpha.1+ci.1`) gera um rascunho de teste que deve ser apagado após a revisão, junto com a tag de teste; execuções manuais só são possíveis depois que o workflow existir na `main`. Verifique um asset gerado pelo workflow com: ```powershell gh attestation verify .\ --repo leoars/PostgresManagementStudio