From d6d9d1ce3c10864d487cbae4f6a2bb2f8da8074f Mon Sep 17 00:00:00 2001 From: cangelosilima Date: Sun, 4 Oct 2026 01:18:53 -0300 Subject: [PATCH 1/4] Publish Windows CLI packages alongside NuGet on main --- .github/scripts/nuget-release.cjs | 49 ++++--- .github/scripts/nuget-release.test.cjs | 28 ++++ .github/scripts/windows-package.cjs | 73 ++++++++++ .github/scripts/windows-package.test.cjs | 33 +++++ .github/workflows/cli-publish-nuget.yml | 16 +++ .github/workflows/cli-publish-windows.yml | 158 ++++++++++++++++++++++ README.md | 3 + cli/docs/cli.md | 4 + cli/src/SyncSql.Cli/SyncSql.Cli.csproj | 7 +- docs/nuget-publishing.md | 5 + docs/windows-packaging.md | 71 ++++++++++ 11 files changed, 427 insertions(+), 20 deletions(-) create mode 100644 .github/scripts/windows-package.cjs create mode 100644 .github/scripts/windows-package.test.cjs create mode 100644 .github/workflows/cli-publish-windows.yml create mode 100644 docs/windows-packaging.md diff --git a/.github/scripts/nuget-release.cjs b/.github/scripts/nuget-release.cjs index fd829c6..7cc54d6 100644 --- a/.github/scripts/nuget-release.cjs +++ b/.github/scripts/nuget-release.cjs @@ -24,9 +24,13 @@ async function reserveTag(options) { const { github, context, tag } = options; requireMainPush(context); if (!await findTag(options)) { - await github.rest.git.createRef({ - ...context.repo, ref: `refs/tags/${tag}`, sha: context.sha - }); + try { + await github.rest.git.createRef({ + ...context.repo, ref: `refs/tags/${tag}`, sha: context.sha + }); + } catch (error) { + if (error.status !== 422 || !await findTag(options)) throw error; + } } } @@ -34,31 +38,38 @@ async function publishRelease(options) { const { github, context, tag, packageId, version, artifacts = 'artifacts' } = options; requireMainPush(context); if (!await findTag(options)) throw new Error('The release tag must be reserved before publishing.'); - const name = `${packageId}.${version}.nupkg`; - const data = fs.readFileSync(path.join(artifacts, name)); + const filenames = options.filenames || [`${packageId}.${version}.nupkg`]; let release; try { ({ data: release } = await github.rest.repos.getReleaseByTag({ ...context.repo, tag })); } catch (error) { if (error.status !== 404) throw error; - ({ data: release } = await github.rest.repos.createRelease({ - ...context.repo, tag_name: tag, target_commitish: context.sha, - name: `${packageId} ${version}`, draft: true, prerelease: false, - generate_release_notes: true - })); + try { + ({ data: release } = await github.rest.repos.createRelease({ + ...context.repo, tag_name: tag, target_commitish: context.sha, + name: `${packageId} ${version}`, draft: true, prerelease: false, + generate_release_notes: true + })); + } catch (createError) { + if (createError.status !== 422) throw createError; + ({ data: release } = await github.rest.repos.getReleaseByTag({ ...context.repo, tag })); + } } const assets = await github.paginate(github.rest.repos.listReleaseAssets, { ...context.repo, release_id: release.id, per_page: 100 }); - const asset = assets.find(item => item.name === name); - if (asset && asset.state !== 'uploaded') { - await github.rest.repos.deleteReleaseAsset({ ...context.repo, asset_id: asset.id }); - } - if (!asset || asset.state !== 'uploaded') { - await github.rest.repos.uploadReleaseAsset({ - ...context.repo, release_id: release.id, name, data, - headers: { 'content-type': 'application/octet-stream', 'content-length': data.length } - }); + for (const name of filenames) { + const data = fs.readFileSync(path.join(artifacts, name)); + const asset = assets.find(item => item.name === name); + if (asset && asset.state !== 'uploaded') { + await github.rest.repos.deleteReleaseAsset({ ...context.repo, asset_id: asset.id }); + } + if (!asset || asset.state !== 'uploaded') { + await github.rest.repos.uploadReleaseAsset({ + ...context.repo, release_id: release.id, name, data, + headers: { 'content-type': 'application/octet-stream', 'content-length': data.length } + }); + } } if (release.draft) { await github.rest.repos.updateRelease({ diff --git a/.github/scripts/nuget-release.test.cjs b/.github/scripts/nuget-release.test.cjs index 14f17df..46a4e2c 100644 --- a/.github/scripts/nuget-release.test.cjs +++ b/.github/scripts/nuget-release.test.cjs @@ -120,3 +120,31 @@ test('replaces an incomplete asset before publishing a recovered draft', async ( await publishRelease(f); assert.deepEqual(f.calls.map(([name]) => name), ['tag', 'delete', 'upload', 'publish']); }); + +test('concurrent publishers reuse a tag and release created by their peer', async () => { + const f = fixture(); + f.github.rest.git.createRef = async args => { + f.state.tag = { object: { type: 'commit', sha: args.sha } }; + throw Object.assign(new Error('Already exists'), { status: 422 }); + }; + f.github.rest.repos.createRelease = async () => { + f.state.release = { id: 7, draft: false }; + throw Object.assign(new Error('Already exists'), { status: 422 }); + }; + await reserveTag(f); + await publishRelease(f); + assert.deepEqual(f.calls.map(([name]) => name), ['upload']); +}); + +test('uploads Windows assets alongside the existing NuGet asset, idempotently', async () => { + const f = fixture(); + await reserveTag(f); + await publishRelease(f); + const filenames = ['syncsql-2026.9.26.42-win-x64.zip', 'syncsql.2026.9.26.42.nupkg', 'syncsql-2026.9.26.42-win-x64.zip.sha256']; + for (const name of filenames) fs.writeFileSync(path.join(artifacts, name), name); + await publishRelease({ ...f, filenames }); + assert.equal(f.state.assets.length, 4); + const calls = f.calls.length; + await publishRelease({ ...f, filenames }); + assert.equal(f.calls.length, calls); +}); diff --git a/.github/scripts/windows-package.cjs b/.github/scripts/windows-package.cjs new file mode 100644 index 0000000..3c399af --- /dev/null +++ b/.github/scripts/windows-package.cjs @@ -0,0 +1,73 @@ +const fs = require('node:fs'); +const path = require('node:path'); +const crypto = require('node:crypto'); + +function generate(version, archive, output) { + const match = /^(\d{4})\.([1-9]|1[0-2])\.([1-9]|[12]\d|3[01])(?:\.([1-9]\d{0,4}))?$/.exec(version); + if (!match || Number(match[4] || 0) > 65534 || Number(match[3]) > new Date(Date.UTC(Number(match[1]), Number(match[2]), 0)).getUTCDate()) { + throw new Error('Windows packages require a stable CalVer YYYY.M.D[.REV].'); + } + const filename = `syncsql-${version}-win-x64.zip`; + if (path.basename(archive) !== filename) throw new Error(`Expected archive ${filename}`); + const hash = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex').toUpperCase(); + const home = 'https://github.com/cangelosilima/SyncSQL'; + const url = `${home}/releases/download/cli-v${version}/${filename}`; + const write = (name, content) => { + const target = path.join(output, name); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, content); + }; + write('chocolatey/syncsql.nuspec', ` + + + syncsql${version} + SQLineage CLI (syncsql)cangelosilima + ${home}${home} + false + SQL Server and Oracle extraction, lineage and catalog CLI. + Self-contained Windows x64 CLI for database extraction, lineage analysis and catalog building. No separate .NET runtime or native Oracle client is required. + sql database lineage oracle cli + + + +`); + write('chocolatey/tools/chocolateyinstall.ps1', `$ErrorActionPreference = 'Stop' +if (-not [Environment]::Is64BitOperatingSystem -or $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { throw 'syncsql requires Windows x64.' } +$toolsDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +Install-ChocolateyZipPackage -PackageName 'syncsql' -Url64bit '${url}' -UnzipLocation $toolsDir -Checksum64 '${hash}' -ChecksumType64 'sha256' +# Only expose the CLI, not other executables shipped by the runtime. +Get-ChildItem $toolsDir -Filter *.exe -Recurse | Where-Object Name -ne 'syncsql.exe' | ForEach-Object { New-Item -ItemType File -Path ($_.FullName + '.ignore') -Force | Out-Null } +`); + const prefix = `PackageIdentifier: cangelosilima.SyncSQL\nPackageVersion: ${version}\n`; + const dir = `winget/manifests/c/cangelosilima/SyncSQL/${version}`; + write(`${dir}/cangelosilima.SyncSQL.yaml`, `${prefix}DefaultLocale: en-US\nManifestType: version\nManifestVersion: 1.6.0\n`); + write(`${dir}/cangelosilima.SyncSQL.locale.en-US.yaml`, `${prefix}PackageLocale: en-US +Publisher: cangelosilima +PackageName: SQLineage CLI +PackageUrl: ${home} +License: Not specified +ShortDescription: SQL Server and Oracle extraction, lineage and catalog CLI. +Moniker: syncsql +ManifestType: defaultLocale +ManifestVersion: 1.6.0 +`); + write(`${dir}/cangelosilima.SyncSQL.installer.yaml`, `${prefix}InstallerType: zip +NestedInstallerType: portable +NestedInstallerFiles: + - RelativeFilePath: syncsql.exe + PortableCommandAlias: syncsql +Installers: + - Architecture: x64 + InstallerUrl: ${url} + InstallerSha256: ${hash} +ManifestType: installer +ManifestVersion: 1.6.0 +`); + write(`${filename}.sha256`, `${hash} ${filename}\n`); +} + +module.exports = { generate }; +if (require.main === module) { + if (process.argv.length !== 5) throw new Error('Usage: node windows-package.cjs VERSION ARCHIVE OUTPUT'); + generate(...process.argv.slice(2)); +} diff --git a/.github/scripts/windows-package.test.cjs b/.github/scripts/windows-package.test.cjs new file mode 100644 index 0000000..5bcb05f --- /dev/null +++ b/.github/scripts/windows-package.test.cjs @@ -0,0 +1,33 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const crypto = require('node:crypto'); +const { generate } = require('./windows-package.cjs'); + +test('both package managers consume the same release URL and archive checksum', (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'syncsql-packaging-')); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const archive = path.join(dir, 'syncsql-2026.10.3.1-win-x64.zip'); + fs.writeFileSync(archive, 'fixture archive'); + const output = path.join(dir, 'output'); + generate('2026.10.3.1', archive, output); + const hash = crypto.createHash('sha256').update('fixture archive').digest('hex').toUpperCase(); + const install = fs.readFileSync(path.join(output, 'chocolatey/tools/chocolateyinstall.ps1'), 'utf8'); + const manifest = fs.readFileSync(path.join(output, 'winget/manifests/c/cangelosilima/SyncSQL/2026.10.3.1/cangelosilima.SyncSQL.installer.yaml'), 'utf8'); + for (const content of [install, manifest]) { + assert.ok(content.includes(hash)); + assert.ok(content.includes('https://github.com/cangelosilima/SyncSQL/releases/download/cli-v2026.10.3.1/syncsql-2026.10.3.1-win-x64.zip')); + } + assert.match(manifest, /RelativeFilePath: syncsql.exe/); + assert.match(manifest, /PortableCommandAlias: syncsql/); + assert.match(install, /ChecksumType64 'sha256'/); +}); + +test('rejects prereleases, invalid dates, unsafe versions and mismatched assets', () => { + for (const version of ['2026.2.29', '2026.04.3', '2026.10.3-rc.1', '2026.10.3.65535', '../1', '2026.13.1']) { + assert.throws(() => generate(version, 'missing.zip', 'unused'), /stable CalVer/); + } + assert.throws(() => generate('2026.10.3', 'wrong.zip', 'unused'), /Expected archive/); +}); diff --git a/.github/workflows/cli-publish-nuget.yml b/.github/workflows/cli-publish-nuget.yml index b8820e3..6fb8b4e 100644 --- a/.github/workflows/cli-publish-nuget.yml +++ b/.github/workflows/cli-publish-nuget.yml @@ -9,6 +9,8 @@ on: - 'global.json' - '.github/workflows/cli-publish-nuget.yml' - '.github/scripts/nuget-release*.cjs' + - '.github/scripts/windows-package*.cjs' + - '.github/workflows/cli-publish-windows.yml' pull_request: paths: - 'cli/**' @@ -16,6 +18,8 @@ on: - 'global.json' - '.github/workflows/cli-publish-nuget.yml' - '.github/scripts/nuget-release*.cjs' + - '.github/scripts/windows-package*.cjs' + - '.github/workflows/cli-publish-windows.yml' workflow_dispatch: permissions: @@ -120,3 +124,15 @@ jobs: github, context, tag: process.env.RELEASE_TAG, packageId: process.env.PACKAGE_ID, version: process.env.PACKAGE_VERSION }); + + windows: + needs: package + uses: ./.github/workflows/cli-publish-windows.yml + permissions: + contents: write + secrets: inherit + with: + version: ${{ needs.package.outputs.version }} + tag: ${{ needs.package.outputs.tag }} + commit: ${{ github.sha }} + publish: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} \ No newline at end of file diff --git a/.github/workflows/cli-publish-windows.yml b/.github/workflows/cli-publish-windows.yml new file mode 100644 index 0000000..639c2ac --- /dev/null +++ b/.github/workflows/cli-publish-windows.yml @@ -0,0 +1,158 @@ +name: Publish Windows CLI packages + +on: + workflow_call: + inputs: + version: + type: string + required: true + tag: + type: string + required: true + commit: + type: string + required: true + publish: + type: boolean + required: true +permissions: + contents: read + +concurrency: + group: windows-cli-${{ github.ref }} + cancel-in-progress: false + +jobs: + package: + runs-on: windows-latest + timeout-minutes: 30 + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ inputs.tag }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit }} + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Use the caller version + id: version + shell: pwsh + env: + CALLER_VERSION: ${{ inputs.version }} + run: | + # Validation NuGet versions have a CI suffix; Windows manifests use stable CalVer. + $version = $env:CALLER_VERSION.Split('-')[0] + "version=$version" >> $env:GITHUB_OUTPUT + - name: Test package generation + run: node --test .github/scripts/windows-package.test.cjs + - name: Publish self-contained CLI + shell: pwsh + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + dotnet publish cli/src/SyncSql.Cli/SyncSql.Cli.csproj -c Release -r win-x64 --self-contained true -p:WindowsStandalone=true -p:Version=$env:VERSION -p:ContinuousIntegrationBuild=true -o artifacts/publish + if ($LASTEXITCODE) { throw 'dotnet publish failed' } + New-Item artifacts/publish/licenses/plsql -ItemType Directory -Force | Out-Null + Copy-Item grammar/src/SyncSql.Grammar.PlSql/Grammar/NOTICE.md, grammar/src/SyncSql.Grammar.PlSql/Grammar/LICENSE.txt artifacts/publish/licenses/plsql + & artifacts/publish/syncsql.exe --help + if ($LASTEXITCODE) { throw 'CLI smoke test failed' } + Compress-Archive artifacts/publish/* "artifacts/syncsql-$env:VERSION-win-x64.zip" + node .github/scripts/windows-package.cjs $env:VERSION "artifacts/syncsql-$env:VERSION-win-x64.zip" artifacts/packages + if ($LASTEXITCODE) { throw 'Manifest generation failed' } + - name: Pack Chocolatey package + shell: pwsh + run: | + choco pack artifacts/packages/chocolatey/syncsql.nuspec --outputdirectory artifacts + if ($LASTEXITCODE) { throw 'Chocolatey pack failed' } + - uses: actions/upload-artifact@v4 + with: + name: syncsql-windows + path: | + artifacts/*.zip + artifacts/*.nupkg + artifacts/packages + if-no-files-found: error + + release: + needs: package + if: inputs.publish && github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: windows-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: syncsql-windows + path: artifacts + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit }} + - name: Prepare checksum asset + shell: pwsh + run: Copy-Item artifacts/packages/*.sha256 artifacts + - name: Publish release assets + uses: actions/github-script@v7 + env: + RELEASE_TAG: ${{ inputs.tag }} + PACKAGE_VERSION: ${{ needs.package.outputs.version }} + with: + script: | + const { reserveTag, publishRelease } = require('./.github/scripts/nuget-release.cjs'); + const version = process.env.PACKAGE_VERSION; + await reserveTag({ github, context, tag: process.env.RELEASE_TAG }); + await publishRelease({ + github, context, tag: process.env.RELEASE_TAG, + packageId: 'SyncSql.Cli', version, + filenames: [ + `syncsql-${version}-win-x64.zip`, + `syncsql.${version}.nupkg`, + `syncsql-${version}-win-x64.zip.sha256` + ] + }); + chocolatey: + needs: [package, release] + runs-on: windows-latest + environment: windows-packages + timeout-minutes: 10 + steps: + - uses: actions/download-artifact@v4 + with: + name: syncsql-windows + path: artifacts + - name: Submit to Chocolatey + shell: pwsh + env: + CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} + run: | + if (-not $env:CHOCO_API_KEY) { throw 'Configure CHOCO_API_KEY in the windows-packages environment.' } + choco push "artifacts/syncsql.${{ needs.package.outputs.version }}.nupkg" --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY + if ($LASTEXITCODE) { throw 'Chocolatey submission failed' } + + winget: + needs: [package, release] + runs-on: windows-latest + environment: windows-packages + timeout-minutes: 10 + steps: + - uses: actions/download-artifact@v4 + with: + name: syncsql-windows + path: artifacts + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '6.0.x' + - name: Submit WinGet manifests + shell: pwsh + env: + WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} + run: | + if (-not $env:WINGET_TOKEN) { throw 'Configure WINGET_TOKEN in the windows-packages environment.' } + Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe + ./wingetcreate.exe submit artifacts/packages/winget/manifests/c/cangelosilima/SyncSQL/${{ needs.package.outputs.version }} --token $env:WINGET_TOKEN + if ($LASTEXITCODE) { throw 'WinGet submission failed' } diff --git a/README.md b/README.md index d5d8844..5133f33 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,9 @@ in `site/src/help/` and are bundled with the site. ![Explorer page guide](docs/screenshots/help-panel.png) +CLI installation: [.NET global tool](cli/docs/cli.md#install) or +[Windows Chocolatey / WinGet release flow](docs/windows-packaging.md). + ## How it works ```mermaid diff --git a/cli/docs/cli.md b/cli/docs/cli.md index af3bf25..f70339b 100644 --- a/cli/docs/cli.md +++ b/cli/docs/cli.md @@ -42,6 +42,10 @@ Windows authentication can instead use the identity running the CLI. ## Install +Windows x64 releases also have a Chocolatey and WinGet packaging flow with +the .NET runtime included. See [Windows package releases](../../docs/windows-packaging.md) +for installation commands, registry availability and maintainer setup. + `syncsql` is published as a [dotnet global tool](https://learn.microsoft.com/dotnet/core/tools/global-tools). ```bash diff --git a/cli/src/SyncSql.Cli/SyncSql.Cli.csproj b/cli/src/SyncSql.Cli/SyncSql.Cli.csproj index 66b6614..7f6601e 100644 --- a/cli/src/SyncSql.Cli/SyncSql.Cli.csproj +++ b/cli/src/SyncSql.Cli/SyncSql.Cli.csproj @@ -1,4 +1,4 @@ - + @@ -44,4 +44,9 @@ + + false + syncsql + + diff --git a/docs/nuget-publishing.md b/docs/nuget-publishing.md index 53b87a3..f5af323 100644 --- a/docs/nuget-publishing.md +++ b/docs/nuget-publishing.md @@ -76,6 +76,11 @@ Pull requests and manual runs only validate, with versions suffixed option. GitHub releases for the two packages do not replace each other as the repository's latest release. +The CLI workflow also calls the reusable Windows packaging workflow alongside +its NuGet publish job. Chocolatey and WinGet use the CLI version, release tag +and tested commit. See [Windows package releases](windows-packaging.md) for +registry credentials, validation and installation commands. + ## Consume ```bash diff --git a/docs/windows-packaging.md b/docs/windows-packaging.md new file mode 100644 index 0000000..25543c5 --- /dev/null +++ b/docs/windows-packaging.md @@ -0,0 +1,71 @@ +# Windows package releases + +`cli-publish-windows.yml` builds a self-contained Windows x64 CLI, runs +`syncsql --help`, packages it as a ZIP, and generates Chocolatey `syncsql` +and WinGet `cangelosilima.SyncSQL` metadata using that ZIP's SHA-256. +The .NET runtime is included. The PL/SQL grammar license and notice travel +with the executable. The repository currently has no application license; +the WinGet metadata says `Not specified`. Decide and document the application +license before the first community submission; do not claim an open-source +license merely because the repository is public. + +## Setup and release + +1. Create the GitHub Actions environment `windows-packages`. Add + `CHOCO_API_KEY` for the Chocolatey account owning `syncsql`, and + `WINGET_TOKEN` for a GitHub account able to fork and submit pull requests + to `microsoft/winget-pkgs`. The default Actions token cannot do this. + Confirm the proposed package identifiers are available before first release. +2. Merge relevant changes into protected `main`. The CLI NuGet workflow calls + the reusable Windows workflow alongside its NuGet publish job. Both consume + the same `YYYY.M.D.BUILD` version, `cli-v*` tag and tested commit. +3. Windows packaging uploads the ZIP, Chocolatey package and checksum to the + CLI GitHub release, then submits to each registry independently. Configure + environment reviewers if release submissions should require review. +4. Wait for Chocolatey moderation and the WinGet pull request to be accepted. + Submission alone does not make the package searchable or installable from + the public catalogs. Monitor the submission outputs for the review links. + +Pull requests and manual runs of the CLI NuGet workflow validate both formats +without publishing. Only a push to `main` may publish; protect that branch with +required reviews and checks so its pushes come from approved merges. Tag pushes +cannot publish. Release assets are immutable: merge a new PR for changed +binaries. If a registry submission fails after assets are uploaded, rerun only +the failed registry job; do not rebuild the release. + +## Install after registry acceptance + +```powershell +choco install syncsql -y +choco upgrade syncsql -y +choco uninstall syncsql -y + +winget install --id cangelosilima.SyncSQL --exact --source winget +winget upgrade --id cangelosilima.SyncSQL --exact --source winget +winget uninstall --id cangelosilima.SyncSQL --exact + +syncsql --help +``` + +Use one installation method per machine to avoid competing `syncsql` commands +on PATH. Other operating systems can use the existing .NET global tool. + +## Local validation + +```powershell +node --test .github/scripts/windows-package.test.cjs +dotnet publish cli/src/SyncSql.Cli/SyncSql.Cli.csproj -c Release -r win-x64 --self-contained true -p:WindowsStandalone=true -p:Version=2026.10.3 -o artifacts/windows/publish +Compress-Archive artifacts/windows/publish/* artifacts/windows/syncsql-2026.10.3-win-x64.zip +node .github/scripts/windows-package.cjs 2026.10.3 artifacts/windows/syncsql-2026.10.3-win-x64.zip artifacts/windows/packages +choco pack artifacts/windows/packages/chocolatey/syncsql.nuspec --outputdirectory artifacts/windows +winget validate artifacts/windows/packages/winget/manifests/c/cangelosilima/SyncSQL/2026.10.3 +``` + +Before the initial submission, test installation, upgrade and removal in a +disposable Windows x64 machine using the public release URL. For a local +Chocolatey package, install with `choco install syncsql --source `. +For WinGet, enable local manifests and use `winget install --manifest `. +These installations still download the published release ZIP. + +The manifests follow [WinGet's manifest format](https://learn.microsoft.com/windows/package-manager/package/manifest) +and submissions use [Microsoft's WingetCreate](https://github.com/microsoft/winget-create). From 50de63ef3e760c7cb123b6da03d3f98b7944bde2 Mon Sep 17 00:00:00 2001 From: cangelosilima Date: Sun, 4 Oct 2026 02:03:53 -0300 Subject: [PATCH 2/4] Verify published CLI installation on fresh runners --- .github/scripts/assert-cli-install.ps1 | 14 ++++ .github/scripts/prepare-winget.ps1 | 12 +++ .github/workflows/cli-publish-nuget.yml | 59 +++++++++++++- .github/workflows/cli-publish-windows.yml | 77 +++++++++++++++++-- .../cli-verify-community-install.yml | 71 +++++++++++++++++ docs/windows-packaging.md | 19 +++++ 6 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/assert-cli-install.ps1 create mode 100644 .github/scripts/prepare-winget.ps1 create mode 100644 .github/workflows/cli-verify-community-install.yml diff --git a/.github/scripts/assert-cli-install.ps1 b/.github/scripts/assert-cli-install.ps1 new file mode 100644 index 0000000..5551422 --- /dev/null +++ b/.github/scripts/assert-cli-install.ps1 @@ -0,0 +1,14 @@ +[CmdletBinding()] +param([Parameter(Mandatory)][string] $Version) +$ErrorActionPreference = 'Stop' +$command = Get-Command syncsql -CommandType Application -ErrorAction Stop +Write-Host "Installed command: $($command.Source)" +$installedVersion = (& $command.Source --version | Out-String).Trim() +if ($LASTEXITCODE -ne 0 -or $installedVersion -notmatch ('^' + [regex]::Escape($Version) + '(\+[^\s]+)?$')) { + throw "Expected syncsql $Version, got '$installedVersion'." +} +$help = & $command.Source --help | Out-String +if ($LASTEXITCODE -ne 0 -or $help -notmatch 'validate-config' -or $help -notmatch 'catalog') { + throw 'The installed CLI did not return its expected help.' +} +Write-Host $help diff --git a/.github/scripts/prepare-winget.ps1 b/.github/scripts/prepare-winget.ps1 new file mode 100644 index 0000000..9a7a4d3 --- /dev/null +++ b/.github/scripts/prepare-winget.ps1 @@ -0,0 +1,12 @@ +$ErrorActionPreference = 'Stop' +$env:PATH = (Join-Path $env:LOCALAPPDATA 'Microsoft/WindowsApps') + [IO.Path]::PathSeparator + $env:PATH +if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + Install-Module -Name Microsoft.WinGet.Client -Repository PSGallery -Scope CurrentUser -Force + Import-Module Microsoft.WinGet.Client + Repair-WinGetPackageManager -AllUsers +} +winget --version +if ($LASTEXITCODE) { throw 'WinGet bootstrap failed.' } +Join-Path $env:LOCALAPPDATA 'Microsoft/WindowsApps' >> $env:GITHUB_PATH +# Portable commands are added to the next workflow step's PATH. +Join-Path $env:LOCALAPPDATA 'Microsoft/WinGet/Links' >> $env:GITHUB_PATH diff --git a/.github/workflows/cli-publish-nuget.yml b/.github/workflows/cli-publish-nuget.yml index 6fb8b4e..265a81b 100644 --- a/.github/workflows/cli-publish-nuget.yml +++ b/.github/workflows/cli-publish-nuget.yml @@ -11,6 +11,8 @@ on: - '.github/scripts/nuget-release*.cjs' - '.github/scripts/windows-package*.cjs' - '.github/workflows/cli-publish-windows.yml' + - '.github/scripts/*-cli-install.ps1' + - '.github/scripts/prepare-winget.ps1' pull_request: paths: - 'cli/**' @@ -20,6 +22,8 @@ on: - '.github/scripts/nuget-release*.cjs' - '.github/scripts/windows-package*.cjs' - '.github/workflows/cli-publish-windows.yml' + - '.github/scripts/*-cli-install.ps1' + - '.github/scripts/prepare-winget.ps1' workflow_dispatch: permissions: @@ -135,4 +139,57 @@ jobs: version: ${{ needs.package.outputs.version }} tag: ${{ needs.package.outputs.tag }} commit: ${{ github.sha }} - publish: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} \ No newline at end of file + publish: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + + verify-nuget-install: + needs: [package, publish] + runs-on: ${{ matrix.os }} + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - name: Wait for the published NuGet version + shell: pwsh + env: + VERSION: ${{ needs.package.outputs.version }} + run: | + $url = "https://api.nuget.org/v3-flatcontainer/syncsql.cli/$env:VERSION/syncsql.cli.$env:VERSION.nupkg" + $visible = $false + for ($attempt = 1; $attempt -le 20; $attempt++) { + try { + Invoke-WebRequest $url -Method Head -TimeoutSec 30 | Out-Null + $visible = $true + break + } catch { + Write-Host "NuGet version not available yet (attempt $attempt/20)." + if ($attempt -lt 20) { Start-Sleep -Seconds 30 } + } + } + if (-not $visible) { throw 'Published package did not become available on nuget.org.' } + - name: Install from nuget.org + shell: pwsh + env: + VERSION: ${{ needs.package.outputs.version }} + run: | + $config = Join-Path $env:RUNNER_TEMP 'install-nuget.config' + '' | Set-Content $config + dotnet tool install --global SyncSql.Cli --version $env:VERSION --configfile $config --no-cache + if ($LASTEXITCODE) { throw 'Published dotnet tool installation failed.' } + Join-Path ([Environment]::GetFolderPath('UserProfile')) '.dotnet/tools' >> $env:GITHUB_PATH + - name: Verify installed command and version + shell: pwsh + env: + VERSION: ${{ needs.package.outputs.version }} + run: ./.github/scripts/assert-cli-install.ps1 -Version $env:VERSION + - name: Uninstall the tool + shell: pwsh + run: | + dotnet tool uninstall --global SyncSql.Cli + if ($LASTEXITCODE) { throw 'dotnet tool uninstall failed.' } + if (Get-Command syncsql -CommandType Application -ErrorAction SilentlyContinue) { throw 'syncsql is still on PATH after uninstall.' } diff --git a/.github/workflows/cli-publish-windows.yml b/.github/workflows/cli-publish-windows.yml index 639c2ac..46a6c01 100644 --- a/.github/workflows/cli-publish-windows.yml +++ b/.github/workflows/cli-publish-windows.yml @@ -86,13 +86,13 @@ jobs: permissions: contents: write steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit }} - uses: actions/download-artifact@v4 with: name: syncsql-windows path: artifacts - - uses: actions/checkout@v4 - with: - ref: ${{ inputs.commit }} - name: Prepare checksum asset shell: pwsh run: Copy-Item artifacts/packages/*.sha256 artifacts @@ -115,9 +115,76 @@ jobs: `syncsql-${version}-win-x64.zip.sha256` ] }); - chocolatey: + + verify-windows-install: needs: [package, release] runs-on: windows-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + manager: [chocolatey, winget] + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit }} + - uses: actions/download-artifact@v4 + with: + name: syncsql-windows + path: artifacts + - name: Install published Chocolatey package + if: matrix.manager == 'chocolatey' + shell: pwsh + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ inputs.tag }} + VERSION: ${{ needs.package.outputs.version }} + run: | + New-Item -ItemType Directory released-package | Out-Null + gh release download $env:RELEASE_TAG --pattern "syncsql.$env:VERSION.nupkg" --dir released-package + if ($LASTEXITCODE) { throw 'Published Chocolatey package download failed.' } + choco install syncsql --version $env:VERSION --source "$PWD/released-package" --yes --no-progress + if ($LASTEXITCODE) { throw 'Chocolatey installation failed.' } + Join-Path $env:ChocolateyInstall 'bin' >> $env:GITHUB_PATH + - name: Prepare WinGet + if: matrix.manager == 'winget' + shell: pwsh + run: ./.github/scripts/prepare-winget.ps1 + - name: Install published ZIP through WinGet + if: matrix.manager == 'winget' + shell: pwsh + env: + VERSION: ${{ needs.package.outputs.version }} + run: | + $manifest = "artifacts/packages/winget/manifests/c/cangelosilima/SyncSQL/$env:VERSION" + winget validate --manifest $manifest + if ($LASTEXITCODE) { throw 'WinGet manifest validation failed.' } + winget settings --enable LocalManifestFiles + if ($LASTEXITCODE) { throw 'Enabling local manifests failed.' } + winget install --manifest $manifest --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity + if ($LASTEXITCODE) { throw 'WinGet installation failed.' } + - name: Verify installed command and version + shell: pwsh + env: + VERSION: ${{ needs.package.outputs.version }} + run: ./.github/scripts/assert-cli-install.ps1 -Version $env:VERSION + - name: Uninstall the package + shell: pwsh + env: + MANAGER: ${{ matrix.manager }} + run: | + if ($env:MANAGER -eq 'chocolatey') { + choco uninstall syncsql --yes --no-progress + } else { + winget uninstall --id cangelosilima.SyncSQL --exact --disable-interactivity + } + if ($LASTEXITCODE) { throw 'Package uninstall failed.' } + if (Get-Command syncsql -CommandType Application -ErrorAction SilentlyContinue) { throw 'syncsql is still on PATH after uninstall.' } + + chocolatey: + needs: [package, release, verify-windows-install] + runs-on: windows-latest environment: windows-packages timeout-minutes: 10 steps: @@ -135,7 +202,7 @@ jobs: if ($LASTEXITCODE) { throw 'Chocolatey submission failed' } winget: - needs: [package, release] + needs: [package, release, verify-windows-install] runs-on: windows-latest environment: windows-packages timeout-minutes: 10 diff --git a/.github/workflows/cli-verify-community-install.yml b/.github/workflows/cli-verify-community-install.yml new file mode 100644 index 0000000..fea862e --- /dev/null +++ b/.github/workflows/cli-verify-community-install.yml @@ -0,0 +1,71 @@ +name: Verify public Windows package installation + +on: + workflow_dispatch: + inputs: + version: + description: Published stable CLI version accepted into both community catalogs + type: string + required: true + +permissions: + contents: read + +jobs: + install: + runs-on: windows-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + manager: [chocolatey, winget] + steps: + - uses: actions/checkout@v4 + - name: Validate requested version + shell: pwsh + env: + VERSION: ${{ inputs.version }} + run: | + if ($env:VERSION -notmatch '^[0-9]{4}\.[0-9]{1,2}\.[0-9]{1,2}(\.[0-9]{1,5})?$') { + throw 'Specify a stable CalVer YYYY.M.D[.REV].' + } + - name: Install from the Chocolatey community catalog + if: matrix.manager == 'chocolatey' + shell: pwsh + env: + VERSION: ${{ inputs.version }} + run: | + choco install syncsql --version $env:VERSION --source https://community.chocolatey.org/api/v2/ --yes --no-progress + if ($LASTEXITCODE) { throw 'Community Chocolatey installation failed. Confirm moderation has completed.' } + Join-Path $env:ChocolateyInstall 'bin' >> $env:GITHUB_PATH + - name: Prepare WinGet + if: matrix.manager == 'winget' + shell: pwsh + run: ./.github/scripts/prepare-winget.ps1 + - name: Install from the WinGet community catalog + if: matrix.manager == 'winget' + shell: pwsh + env: + VERSION: ${{ inputs.version }} + run: | + winget source update --name winget --disable-interactivity + if ($LASTEXITCODE) { throw 'WinGet source refresh failed.' } + winget install --id cangelosilima.SyncSQL --exact --version $env:VERSION --source winget --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity + if ($LASTEXITCODE) { throw 'Community WinGet installation failed. Confirm the manifest PR has merged and the index has updated.' } + - name: Verify installed command and version + shell: pwsh + env: + VERSION: ${{ inputs.version }} + run: ./.github/scripts/assert-cli-install.ps1 -Version $env:VERSION + - name: Uninstall and check removal + shell: pwsh + env: + MANAGER: ${{ matrix.manager }} + run: | + if ($env:MANAGER -eq 'chocolatey') { + choco uninstall syncsql --yes --no-progress + } else { + winget uninstall --id cangelosilima.SyncSQL --exact --disable-interactivity + } + if ($LASTEXITCODE) { throw 'Package uninstall failed.' } + if (Get-Command syncsql -CommandType Application -ErrorAction SilentlyContinue) { throw 'syncsql is still on PATH after uninstall.' } diff --git a/docs/windows-packaging.md b/docs/windows-packaging.md index 25543c5..fce76ec 100644 --- a/docs/windows-packaging.md +++ b/docs/windows-packaging.md @@ -35,6 +35,25 @@ the failed registry job; do not rebuild the release. ## Install after registry acceptance +After the NuGet publish job succeeds, three fresh runners (Linux, Windows and +macOS) install that exact version with `dotnet tool install --global` from +nuget.org. The check waits up to ten minutes for NuGet propagation, then runs +`syncsql --version`, `syncsql --help`, and uninstall, checking command removal. + +After GitHub release publication, two separate Windows runners install using +Chocolatey and WinGet. Chocolatey downloads the published `.nupkg` from the +release into a local feed; WinGet uses the submitted local manifests. Both +package managers download and verify the real public release ZIP. Each checks +the installed version, command discovery on PATH, help, and removal. Community +submissions wait for both checks to pass. These checks exercise installation +without waiting for community moderation, which can take longer than a CI run. + +Once both community catalogs accept a version, run **Verify public Windows +package installation** (`cli-verify-community-install.yml`) with that version. +It installs directly from the public Chocolatey and WinGet catalogs on two +fresh runners, verifies the version and help, and tests uninstall. No workflow +publishes from this manual verification run. + ```powershell choco install syncsql -y choco upgrade syncsql -y From 8b8c3608464e16a24fe6521a9b1cc742edb40b86 Mon Sep 17 00:00:00 2001 From: cangelosilima Date: Sun, 4 Oct 2026 02:06:47 -0300 Subject: [PATCH 3/4] Install release checks directly from public package catalogs --- .github/workflows/cli-publish-windows.yml | 77 +++---------------- .../cli-verify-community-install.yml | 37 +++++++-- docs/windows-packaging.md | 30 ++++---- 3 files changed, 56 insertions(+), 88 deletions(-) diff --git a/.github/workflows/cli-publish-windows.yml b/.github/workflows/cli-publish-windows.yml index 46a6c01..5523eb1 100644 --- a/.github/workflows/cli-publish-windows.yml +++ b/.github/workflows/cli-publish-windows.yml @@ -116,74 +116,8 @@ jobs: ] }); - verify-windows-install: - needs: [package, release] - runs-on: windows-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - manager: [chocolatey, winget] - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ inputs.commit }} - - uses: actions/download-artifact@v4 - with: - name: syncsql-windows - path: artifacts - - name: Install published Chocolatey package - if: matrix.manager == 'chocolatey' - shell: pwsh - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - RELEASE_TAG: ${{ inputs.tag }} - VERSION: ${{ needs.package.outputs.version }} - run: | - New-Item -ItemType Directory released-package | Out-Null - gh release download $env:RELEASE_TAG --pattern "syncsql.$env:VERSION.nupkg" --dir released-package - if ($LASTEXITCODE) { throw 'Published Chocolatey package download failed.' } - choco install syncsql --version $env:VERSION --source "$PWD/released-package" --yes --no-progress - if ($LASTEXITCODE) { throw 'Chocolatey installation failed.' } - Join-Path $env:ChocolateyInstall 'bin' >> $env:GITHUB_PATH - - name: Prepare WinGet - if: matrix.manager == 'winget' - shell: pwsh - run: ./.github/scripts/prepare-winget.ps1 - - name: Install published ZIP through WinGet - if: matrix.manager == 'winget' - shell: pwsh - env: - VERSION: ${{ needs.package.outputs.version }} - run: | - $manifest = "artifacts/packages/winget/manifests/c/cangelosilima/SyncSQL/$env:VERSION" - winget validate --manifest $manifest - if ($LASTEXITCODE) { throw 'WinGet manifest validation failed.' } - winget settings --enable LocalManifestFiles - if ($LASTEXITCODE) { throw 'Enabling local manifests failed.' } - winget install --manifest $manifest --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity - if ($LASTEXITCODE) { throw 'WinGet installation failed.' } - - name: Verify installed command and version - shell: pwsh - env: - VERSION: ${{ needs.package.outputs.version }} - run: ./.github/scripts/assert-cli-install.ps1 -Version $env:VERSION - - name: Uninstall the package - shell: pwsh - env: - MANAGER: ${{ matrix.manager }} - run: | - if ($env:MANAGER -eq 'chocolatey') { - choco uninstall syncsql --yes --no-progress - } else { - winget uninstall --id cangelosilima.SyncSQL --exact --disable-interactivity - } - if ($LASTEXITCODE) { throw 'Package uninstall failed.' } - if (Get-Command syncsql -CommandType Application -ErrorAction SilentlyContinue) { throw 'syncsql is still on PATH after uninstall.' } - chocolatey: - needs: [package, release, verify-windows-install] + needs: [package, release] runs-on: windows-latest environment: windows-packages timeout-minutes: 10 @@ -202,7 +136,7 @@ jobs: if ($LASTEXITCODE) { throw 'Chocolatey submission failed' } winget: - needs: [package, release, verify-windows-install] + needs: [package, release] runs-on: windows-latest environment: windows-packages timeout-minutes: 10 @@ -223,3 +157,10 @@ jobs: Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe ./wingetcreate.exe submit artifacts/packages/winget/manifests/c/cangelosilima/SyncSQL/${{ needs.package.outputs.version }} --token $env:WINGET_TOKEN if ($LASTEXITCODE) { throw 'WinGet submission failed' } + + verify-community-install: + needs: [package, chocolatey, winget] + uses: ./.github/workflows/cli-verify-community-install.yml + with: + version: ${{ needs.package.outputs.version }} + commit: ${{ inputs.commit }} \ No newline at end of file diff --git a/.github/workflows/cli-verify-community-install.yml b/.github/workflows/cli-verify-community-install.yml index fea862e..3f691f3 100644 --- a/.github/workflows/cli-verify-community-install.yml +++ b/.github/workflows/cli-verify-community-install.yml @@ -1,6 +1,14 @@ name: Verify public Windows package installation on: + workflow_call: + inputs: + version: + type: string + required: true + commit: + type: string + required: true workflow_dispatch: inputs: version: @@ -14,13 +22,15 @@ permissions: jobs: install: runs-on: windows-latest - timeout-minutes: 20 + timeout-minutes: 25 strategy: fail-fast: false matrix: manager: [chocolatey, winget] steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit || github.sha }} - name: Validate requested version shell: pwsh env: @@ -35,8 +45,14 @@ jobs: env: VERSION: ${{ inputs.version }} run: | - choco install syncsql --version $env:VERSION --source https://community.chocolatey.org/api/v2/ --yes --no-progress - if ($LASTEXITCODE) { throw 'Community Chocolatey installation failed. Confirm moderation has completed.' } + $installed = $false + for ($attempt = 1; $attempt -le 20; $attempt++) { + choco install syncsql --version $env:VERSION --source https://community.chocolatey.org/api/v2/ --yes --no-progress + if ($LASTEXITCODE -eq 0) { $installed = $true; break } + Write-Host "Chocolatey public installation failed (attempt $attempt/20); awaiting moderation or retrying a transient failure." + if ($attempt -lt 20) { Start-Sleep -Seconds 30 } + } + if (-not $installed) { throw 'Exact-version installation from Chocolatey failed. Once moderation completes, rerun this verification job.' } Join-Path $env:ChocolateyInstall 'bin' >> $env:GITHUB_PATH - name: Prepare WinGet if: matrix.manager == 'winget' @@ -48,10 +64,17 @@ jobs: env: VERSION: ${{ inputs.version }} run: | - winget source update --name winget --disable-interactivity - if ($LASTEXITCODE) { throw 'WinGet source refresh failed.' } - winget install --id cangelosilima.SyncSQL --exact --version $env:VERSION --source winget --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity - if ($LASTEXITCODE) { throw 'Community WinGet installation failed. Confirm the manifest PR has merged and the index has updated.' } + $installed = $false + for ($attempt = 1; $attempt -le 20; $attempt++) { + winget source update --name winget --disable-interactivity + if ($LASTEXITCODE -eq 0) { + winget install --id cangelosilima.SyncSQL --exact --version $env:VERSION --source winget --scope user --accept-package-agreements --accept-source-agreements --disable-interactivity + if ($LASTEXITCODE -eq 0) { $installed = $true; break } + } + Write-Host "WinGet public installation failed (attempt $attempt/20); awaiting catalog approval/indexing or retrying a transient failure." + if ($attempt -lt 20) { Start-Sleep -Seconds 30 } + } + if (-not $installed) { throw 'Exact-version installation from WinGet failed. Once the catalog contains this version, rerun this verification job.' } - name: Verify installed command and version shell: pwsh env: diff --git a/docs/windows-packaging.md b/docs/windows-packaging.md index fce76ec..be2e08a 100644 --- a/docs/windows-packaging.md +++ b/docs/windows-packaging.md @@ -40,19 +40,25 @@ macOS) install that exact version with `dotnet tool install --global` from nuget.org. The check waits up to ten minutes for NuGet propagation, then runs `syncsql --version`, `syncsql --help`, and uninstall, checking command removal. -After GitHub release publication, two separate Windows runners install using -Chocolatey and WinGet. Chocolatey downloads the published `.nupkg` from the -release into a local feed; WinGet uses the submitted local manifests. Both -package managers download and verify the real public release ZIP. Each checks -the installed version, command discovery on PATH, help, and removal. Community -submissions wait for both checks to pass. These checks exercise installation -without waiting for community moderation, which can take longer than a CI run. +After Chocolatey and WinGet submissions succeed, two separate Windows runners +install the exact published version directly from their public catalogs: +Chocolatey uses `https://community.chocolatey.org/api/v2/`; WinGet uses +`--source winget` with the package ID and version. No local feeds, local +manifests or release-asset fallback are used by these installation checks. +Each checks the installed version, command discovery on PATH, help, and removal. + +Windows catalog installation retries up to twenty times, thirty seconds apart. +Community moderation may take longer than this window. If the exact version +remains unavailable, the verification job fails with a message to rerun after +approval; registry submission jobs remain successful. No older version is used +to make the test pass. Once both community catalogs accept a version, run **Verify public Windows package installation** (`cli-verify-community-install.yml`) with that version. It installs directly from the public Chocolatey and WinGet catalogs on two fresh runners, verifies the version and help, and tests uninstall. No workflow -publishes from this manual verification run. +publishes from this manual verification run. The release workflow also calls +this same verification workflow automatically after registry submission. ```powershell choco install syncsql -y @@ -80,11 +86,9 @@ choco pack artifacts/windows/packages/chocolatey/syncsql.nuspec --outputdirector winget validate artifacts/windows/packages/winget/manifests/c/cangelosilima/SyncSQL/2026.10.3 ``` -Before the initial submission, test installation, upgrade and removal in a -disposable Windows x64 machine using the public release URL. For a local -Chocolatey package, install with `choco install syncsql --source `. -For WinGet, enable local manifests and use `winget install --manifest `. -These installations still download the published release ZIP. +Before marking a release verified, require its public-source installation jobs +to pass. Local packaging and manifest validation alone do not verify that the +release is installable from the public catalogs. The manifests follow [WinGet's manifest format](https://learn.microsoft.com/windows/package-manager/package/manifest) and submissions use [Microsoft's WingetCreate](https://github.com/microsoft/winget-create). From 321193b0c838df48a2b147b4379b381a76e651ab Mon Sep 17 00:00:00 2001 From: cangelosilima Date: Sun, 4 Oct 2026 02:11:10 -0300 Subject: [PATCH 4/4] Update brace expansion dependencies to clear CI audit --- site/package-lock.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/site/package-lock.json b/site/package-lock.json index 48a3a57..92403d9 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -1381,9 +1381,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4275,9 +4275,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4861,9 +4861,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -5744,9 +5744,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7806,9 +7806,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0"