Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion .github/workflows/builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,54 @@ jobs:
archive: false


pkg-winget:
if: startsWith(github.ref, 'refs/tags/')
runs-on: windows-2025
needs: [get-version, pkg-msi]
steps:
- uses: actions/checkout@main
- uses: actions/download-artifact@main
with:
name: "snclient-${{needs.get-version.outputs.version}}-windows-i386.msi"
path: "winget-msi/i386"
- uses: actions/download-artifact@main
with:
name: "snclient-${{needs.get-version.outputs.version}}-windows-x86_64.msi"
path: "winget-msi/x86_64"
- uses: actions/download-artifact@main
with:
name: "snclient-${{needs.get-version.outputs.version}}-windows-aarch64.msi"
path: "winget-msi/aarch64"
- name: "Generate and validate WinGet manifests"
shell: pwsh
run: |
$version = "${{ needs.get-version.outputs.version }}"
$packageVersion = "${{ needs.get-version.outputs.major }}.${{ needs.get-version.outputs.minor }}.${{ needs.get-version.outputs.revision }}"
$msiX86 = "winget-msi/i386/snclient-$version-windows-i386.msi"
$msiX64 = "winget-msi/x86_64/snclient-$version-windows-x86_64.msi"
$msiArm64 = "winget-msi/aarch64/snclient-$version-windows-aarch64.msi"
$releaseDate = git log -1 --format=%cs
& .\buildtools\winget_manifests.ps1 `
-PackageVersion $packageVersion `
-TagVersion $version `
-ReleaseDate $releaseDate `
-MsiX64 $msiX64 `
-MsiX86 $msiX86 `
-MsiArm64 $msiArm64 `
-OutDir "packaging\winget\$packageVersion"
if ($LASTEXITCODE -ne 0) { exit 1 }
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { throw 'winget is not available.' }
winget validate --manifest "packaging\winget\$packageVersion"
if ($LASTEXITCODE -ne 0) { exit 1 }
Compress-Archive -Path "packaging\winget\$packageVersion\*" -DestinationPath "snclient-$version-winget-manifests.zip" -Force
- uses: actions/upload-artifact@main
with:
name: "winget-manifests"
path: "snclient-${{ needs.get-version.outputs.version }}-winget-manifests.zip"
if-no-files-found: error
archive: false


pkg-deb:
strategy:
fail-fast: false
Expand Down Expand Up @@ -860,7 +908,7 @@ jobs:
make-release:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-26.04
needs: [get-version,pkg-msi,pkg-deb,pkg-rpm,pkg-osx,integration-test-linux,integration-test-windows, integration-test-osx, test-linux, test-win, test-osx, test-bsd, test-common, test-bench, test-cross, test-docs]
needs: [get-version,pkg-msi,pkg-winget,pkg-deb,pkg-rpm,pkg-osx,integration-test-linux,integration-test-windows, integration-test-osx, test-linux, test-win, test-osx, test-bsd, test-common, test-bench, test-cross, test-docs]
permissions:
contents: write
outputs:
Expand Down Expand Up @@ -899,6 +947,21 @@ jobs:
- run: gh release upload v${{needs.get-version.outputs.version}} ${{ env.BIN }}.msi


upload-winget-manifests:
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-26.04
needs: [get-version, make-release, pkg-winget]
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/download-artifact@main
with:
name: "winget-manifests"
- run: gh release upload v${{needs.get-version.outputs.version}} snclient-${{needs.get-version.outputs.version}}-winget-manifests.zip


upload-linux-deb-assets:
if: startsWith(github.ref, 'refs/tags/')
strategy:
Expand Down
187 changes: 187 additions & 0 deletions .github/workflows/winget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
name: Publish to WinGet

# This workflow publishes the generated WinGet manifests to microsoft/winget-pkgs.
# It does not generate the manifests.
# The release workflow attaches the manifest zip to the release.
# The API creates the commit and PR in the fork without cloning the repository.
# Set the repository variable WINGET_FORK_OWNER to the account that owns the fork.
# Set the repository variable WINGET_AUTOPUBLISH to true after the first submission.
# The WINGET_TOKEN secret needs a classic PAT with public_repo scope.

on:
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to submit (for example v1.02)"
required: true
type: string
dry_run:
description: "Print the manifests and do not create a PR"
required: false
default: true
type: boolean

permissions:
contents: read

concurrency:
group: winget-${{ github.event.release.tag_name || inputs.release_tag }}
cancel-in-progress: false

jobs:
publish:
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && !github.event.release.prerelease && vars.WINGET_AUTOPUBLISH == 'true')
runs-on: windows-2025
env:
GH_TOKEN: ${{ secrets.WINGET_TOKEN }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.release_tag }}
WINGET_FORK_OWNER: ${{ vars.WINGET_FORK_OWNER }}
steps:
- name: "Download release manifests"
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$tag = $env:RELEASE_TAG
if (-not $tag) { throw 'No release tag was provided.' }
$tagVersion = $tag -replace '^v', ''
$downloadDir = Join-Path $env:RUNNER_TEMP 'winget-download'
$manifestDir = Join-Path $env:RUNNER_TEMP 'winget-manifests'
New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null
New-Item -ItemType Directory -Force -Path $manifestDir | Out-Null
& gh release download $tag --repo $env:GH_REPO --pattern "snclient-$tagVersion-winget-manifests.zip" --dir $downloadDir
if ($LASTEXITCODE -ne 0) { throw "Could not download the manifest asset for $tag." }
$zip = Get-ChildItem -Path $downloadDir -Filter '*.zip' | Select-Object -First 1
if (-not $zip) { throw 'The manifest zip is missing.' }
Expand-Archive -Path $zip.FullName -DestinationPath $manifestDir -Force
Get-ChildItem -Path $manifestDir -Filter '*.yaml' | Select-Object Name, Length

- name: "Dry run"
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
shell: pwsh
run: |
$manifestDir = Join-Path $env:RUNNER_TEMP 'winget-manifests'
Get-ChildItem -Path $manifestDir -Filter '*.yaml' | ForEach-Object {
Write-Output "===== $($_.Name) ====="
Get-Content -LiteralPath $_.FullName
}

- name: "Publish manifests"
if: ${{ github.event_name != 'workflow_dispatch' || inputs.dry_run != true }}
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$manifestDir = Join-Path $env:RUNNER_TEMP 'winget-manifests'
$forkOwner = $env:WINGET_FORK_OWNER
if (-not $forkOwner) { throw 'Set the WINGET_FORK_OWNER repository variable.' }
$repo = 'microsoft/winget-pkgs'
$forkRepo = "$forkOwner/winget-pkgs"

function Invoke-GhJson {
param([string[]]$Arguments)
$output = & gh @Arguments
if ($LASTEXITCODE -ne 0) {
throw "gh api failed: $($Arguments -join ' ')"
}
if (-not $output) { return $null }
return ($output | ConvertFrom-Json)
}

$versionFile = Join-Path $manifestDir 'ConSol.SNClient.yaml'
if (-not (Test-Path -LiteralPath $versionFile)) { throw 'ConSol.SNClient.yaml is missing.' }
$versionMatch = Select-String -Path $versionFile -Pattern '^PackageVersion:\s*(.+)$'
if (-not $versionMatch) { throw 'PackageVersion is missing.' }
$packageVersion = $versionMatch.Matches[0].Groups[1].Value.Trim()
$identifierMatch = Select-String -Path $versionFile -Pattern '^PackageIdentifier:\s*(.+)$'
if (-not $identifierMatch) { throw 'PackageIdentifier is missing.' }
$packageIdentifier = $identifierMatch.Matches[0].Groups[1].Value.Trim()
if ($packageIdentifier -ne 'ConSol.SNClient') { throw "Unexpected package identifier: $packageIdentifier" }

Invoke-GhJson -Arguments @('api', "repos/$forkRepo") | Out-Null

& gh api "repos/$repo/contents/manifests/c/ConSol/SNClient/$packageVersion" 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Output "Version $packageVersion already exists in winget-pkgs."
exit 0
}

& gh api "repos/$repo/contents/manifests/c/ConSol/SNClient" 2>$null | Out-Null
$isNewPackage = $LASTEXITCODE -ne 0
$title = if ($isNewPackage) { "New package: $packageIdentifier $packageVersion" } else { "Update $packageIdentifier to $packageVersion" }
$message = if ($isNewPackage) { "New package: $packageIdentifier $packageVersion" } else { "Update $packageIdentifier to $packageVersion" }

$upstreamRef = Invoke-GhJson -Arguments @('api', "repos/$repo/git/ref/heads/master")
$forkRef = Invoke-GhJson -Arguments @('api', "repos/$forkRepo/git/ref/heads/master")
if ($forkRef.object.sha -ne $upstreamRef.object.sha) {
$syncPayload = @{ branch = 'master' } | ConvertTo-Json
$syncFile = Join-Path $env:RUNNER_TEMP 'winget-sync.json'
[IO.File]::WriteAllText($syncFile, $syncPayload, [Text.UTF8Encoding]::new($false))
& gh api --method POST "repos/$forkRepo/merge-upstream" --input $syncFile 2>$null | Out-Null
$forkRef = Invoke-GhJson -Arguments @('api', "repos/$forkRepo/git/ref/heads/master")
}
if ($forkRef.object.sha -ne $upstreamRef.object.sha) {
throw 'The winget-pkgs fork is not current. Sync it and retry.'
}
$parentSha = $forkRef.object.sha
$forkCommit = Invoke-GhJson -Arguments @('api', "repos/$forkRepo/git/commits/$parentSha")
$baseTree = $forkCommit.tree.sha

$tempDir = Join-Path $env:RUNNER_TEMP 'winget-pr'
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
$utf8 = New-Object System.Text.UTF8Encoding($false)
$entries = @()
Get-ChildItem -Path $manifestDir -Filter '*.yaml' | ForEach-Object {
$content = [Convert]::ToBase64String([IO.File]::ReadAllBytes($_.FullName))
$blobPayload = @{ content = $content; encoding = 'base64' } | ConvertTo-Json -Compress
$blobFile = Join-Path $tempDir ("blob-" + $_.Name + ".json")
[IO.File]::WriteAllText($blobFile, $blobPayload, $utf8)
$blob = Invoke-GhJson -Arguments @('api', '--method', 'POST', "repos/$forkRepo/git/blobs", '--input', $blobFile)
$entries += @{
path = "manifests/c/ConSol/SNClient/$packageVersion/$($_.Name)"
mode = '100644'
type = 'blob'
sha = $blob.sha
}
}

$treePayload = @{ base_tree = $baseTree; tree = $entries } | ConvertTo-Json -Depth 6
$treeFile = Join-Path $tempDir 'tree.json'
[IO.File]::WriteAllText($treeFile, $treePayload, $utf8)
$tree = Invoke-GhJson -Arguments @('api', '--method', 'POST', "repos/$forkRepo/git/trees", '--input', $treeFile)

$commitPayload = @{
message = $message
tree = $tree.sha
parents = @($parentSha)
} | ConvertTo-Json -Depth 4
$commitFile = Join-Path $tempDir 'commit.json'
[IO.File]::WriteAllText($commitFile, $commitPayload, $utf8)
$commit = Invoke-GhJson -Arguments @('api', '--method', 'POST', "repos/$forkRepo/git/commits", '--input', $commitFile)

$branch = "consol-snclient-$packageVersion-$env:GITHUB_RUN_ID"
$refPayload = @{ ref = "refs/heads/$branch"; sha = $commit.sha } | ConvertTo-Json
$refFile = Join-Path $tempDir 'ref.json'
[IO.File]::WriteAllText($refFile, $refPayload, $utf8)
Invoke-GhJson -Arguments @('api', '--method', 'POST', "repos/$forkRepo/git/refs", '--input', $refFile) | Out-Null

$body = @"
$title

SNClient is a general-purpose monitoring agent.
The installer is a per-machine WiX MSI.
It installs the snclient Windows service and a firewall rule.
ElevationRequirement is elevatesSelf because elevationRequired triggers microsoft/winget-cli#2231.
InstallationMetadata runs snclient.exe --version for executable discovery.
"@
$prPayload = @{
title = $title
head = "${forkOwner}:${branch}"
base = 'master'
body = $body
} | ConvertTo-Json -Depth 4
$prFile = Join-Path $tempDir 'pr.json'
[IO.File]::WriteAllText($prFile, $prPayload, $utf8)
$pr = Invoke-GhJson -Arguments @('api', '--method', 'POST', "repos/$repo/pulls", '--input', $prFile)
Write-Output "Created PR $($pr.html_url)"
21 changes: 20 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export PATH := $(GOBIN):$(PATH)
GOLANG_CI_OPTIONS=--show-stats=0 --uniq-by-line=0 --timeout=5m

VERSION ?= $(shell ./buildtools/get_version)
WINGET_PACKAGE_VERSION ?= $(shell ./buildtools/get_winget_version)
WINGET_TAG_VERSION ?= $(VERSION)
WINGET_RELEASE_DATE ?= $(shell git log -1 --format=%cs)
WINGET_OUTDIR ?= $(if $(WINGET_PACKAGE_VERSION),packaging/winget/$(WINGET_PACKAGE_VERSION),)
GOARCH ?= $(shell go env GOARCH)
GOOS ?= $(shell go env GOOS)

Expand Down Expand Up @@ -117,7 +121,7 @@ endif

ENTR=ls cmd/*/*.go pkg/*/*.go pkg/*/*/*.go snclient*.ini | entr -sr

.PHONY=docs
.PHONY=docs winget-manifests

all: build snclient.ini server.crt server.key

Expand Down Expand Up @@ -363,6 +367,7 @@ clean:
rm -rf $(TOOLSFOLDER)
rm -rf dist/
rm -rf windist/
rm -rf packaging/winget/[0-9]*.[0-9]*.[0-9]*
rm -rf build-deb/
rm -rf build-rpm/
rm -f release_notes.txt
Expand Down Expand Up @@ -473,6 +478,20 @@ windist: | dist
windist/snclient.ini
todos windist/snclient.ini

# Generate the WinGet manifests for the current version.
winget-manifests:
WINGET_PACKAGE_VERSION="$(WINGET_PACKAGE_VERSION)" \
WINGET_TAG_VERSION="$(WINGET_TAG_VERSION)" \
WINGET_RELEASE_DATE="$(WINGET_RELEASE_DATE)" \
WINGET_OUTDIR="$(WINGET_OUTDIR)" \
WINGET_SHA256_X64="$(WINGET_SHA256_X64)" \
WINGET_SHA256_X86="$(WINGET_SHA256_X86)" \
WINGET_SHA256_ARM64="$(WINGET_SHA256_ARM64)" \
WINGET_PRODUCTCODE_X64="$(WINGET_PRODUCTCODE_X64)" \
WINGET_PRODUCTCODE_X86="$(WINGET_PRODUCTCODE_X86)" \
WINGET_PRODUCTCODE_ARM64="$(WINGET_PRODUCTCODE_ARM64)" \
./buildtools/winget_manifests.sh


snclient: build snclient.ini

Expand Down
21 changes: 21 additions & 0 deletions buildtools/get_winget_version
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/sh

# Print the WinGet package version.
# This version comes from the MSI ProductVersion and ARP DisplayVersion.
# The WinGet PackageVersion must match the installed MSI version.

set -eu

SCRIPT_DIR=$(CDPATH='' cd "$(dirname "$0")" && pwd)
ROOT=$(dirname "$SCRIPT_DIR")
VERSION=$(cd "$ROOT" && ./buildtools/get_version)
PARTS=$(printf '%s' "$VERSION" | awk -F. '{ print NF }')

if [ "$PARTS" -eq 2 ]; then
VERSION="${VERSION}.0"
elif [ "$PARTS" -ne 3 ]; then
echo "unsupported version: $VERSION" >&2
exit 1
fi

echo "$VERSION"
Loading
Loading