Skip to content

Commit f920b92

Browse files
committed
packaging: drop Azure requirement for Windows signing — support any-CA PFX cert + add winget manifest and auto-PR job (free, no SmartScreen warning)
1 parent 6bd0dba commit f920b92

6 files changed

Lines changed: 191 additions & 64 deletions

File tree

.github/workflows/release.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,16 +227,20 @@ jobs:
227227
shell: pwsh
228228
env:
229229
VERSION: ${{ needs.prepare.outputs.version }}
230-
# Azure Trusted Signing — set these as repo secrets to produce a
231-
# SmartScreen-trusted, signed .exe + .msi. Absent → unsigned MSI.
230+
# Code signing is OPTIONAL. Two ways, pick whichever you have:
231+
# (a) any-CA cert: base64 your .pfx into WINDOWS_CERT_BASE64 (+ password)
232+
# (b) Azure Trusted Signing: set the TRUSTED_SIGNING_* secrets
233+
# No secrets → a valid UNSIGNED installer is still produced.
234+
WINDOWS_CERT_BASE64: ${{ secrets.WINDOWS_CERT_BASE64 }}
235+
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
232236
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
233237
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
234238
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
235239
TRUSTED_SIGNING_ENDPOINT: ${{ secrets.TRUSTED_SIGNING_ENDPOINT }}
236240
TRUSTED_SIGNING_ACCOUNT: ${{ secrets.TRUSTED_SIGNING_ACCOUNT }}
237241
TRUSTED_SIGNING_PROFILE: ${{ secrets.TRUSTED_SIGNING_PROFILE }}
238242
run: |
239-
$sign = if ($env:TRUSTED_SIGNING_ENDPOINT) { '-Sign' } else { '' }
243+
$sign = if ($env:WINDOWS_CERT_BASE64 -or $env:TRUSTED_SIGNING_ENDPOINT) { '-Sign' } else { '' }
240244
./packaging/windows/build-msi.ps1 `
241245
-Version "$env:VERSION" `
242246
-Exe agentty-windows-x86_64.exe `
@@ -418,3 +422,23 @@ jobs:
418422
rm -f SHA256SUMS
419423
sha256sum -- * > SHA256SUMS
420424
gh release upload "$TAG" SHA256SUMS --clobber
425+
426+
# Submit the Windows .msi to the winget community repo. winget is the
427+
# zero-cost, no-SmartScreen-warning install path: `winget install agentty`.
428+
# The action computes the installer SHA from the published .msi and opens a
429+
# PR to microsoft/winget-pkgs. Requires a PAT (classic, public_repo scope)
430+
# stored as the WINGET_TOKEN secret; skipped automatically if absent.
431+
publish-winget:
432+
name: publish to winget
433+
needs: [prepare, build-windows]
434+
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
435+
runs-on: ubuntu-latest
436+
steps:
437+
- name: Submit manifest to winget-pkgs
438+
if: ${{ secrets.WINGET_TOKEN != '' }}
439+
uses: vedantmgoyal9/winget-releaser@main
440+
with:
441+
identifier: agentty.agentty
442+
version: ${{ needs.prepare.outputs.version }}
443+
installers-regex: 'agentty-windows-x86_64\.msi$'
444+
token: ${{ secrets.WINGET_TOKEN }}

packaging/windows/README.md

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,56 @@ That emits `agentty-windows-x86_64.msi` (unsigned). Add `-Sign` to sign it (see
2323

2424
## Code signing — getting rid of the SmartScreen warning
2525

26-
An unsigned installer triggers Windows SmartScreen ("unknown publisher"). To
27-
ship a **trusted, signed** installer you need a publicly-trusted code-signing
28-
certificate. Since 2023 the private key must live on FIPS-140-2 hardware/HSM,
29-
so signing in CI is done through a cloud signing service. We use
30-
**[Azure Trusted Signing](https://learn.microsoft.com/azure/trusted-signing/)**
31-
(~\$10/month; EV-validated profiles are trusted by SmartScreen immediately).
32-
33-
### One-time setup
34-
35-
1. Create a **Trusted Signing account** + a **certificate profile** in Azure.
36-
2. Create an **app registration (service principal)** and grant it the
37-
*Trusted Signing Certificate Profile Signer* role on the account.
38-
3. Add these as **GitHub Actions repository secrets**:
39-
40-
| Secret | Value |
41-
|--------|-------|
42-
| `AZURE_TENANT_ID` | the service-principal tenant id |
43-
| `AZURE_CLIENT_ID` | the service-principal app id |
44-
| `AZURE_CLIENT_SECRET` | the service-principal secret |
45-
| `TRUSTED_SIGNING_ENDPOINT` | e.g. `https://wus2.codesigning.azure.net` |
46-
| `TRUSTED_SIGNING_ACCOUNT` | your Trusted Signing account name |
47-
| `TRUSTED_SIGNING_PROFILE` | your certificate profile name |
48-
49-
With those secrets present, the release workflow signs **both** `agentty.exe`
50-
and the `.msi`. Without them, it still publishes a valid **unsigned** MSI.
51-
52-
> Alternatives to Azure Trusted Signing: DigiCert KeyLocker, SSL.com eSigner —
53-
> any service that exposes a signtool dlib works; adjust `build-msi.ps1`
54-
> accordingly.
26+
An unsigned installer triggers Windows SmartScreen ("unknown publisher"). You
27+
have three ways to deal with this — **you do not need Azure**:
28+
29+
### Option A (free, no signing): ship via winget / Scoop
30+
31+
Package managers download from the GitHub release and install with **no
32+
SmartScreen warning at all** — the manager is trusted, not your binary. For a
33+
CLI this is what most users prefer:
34+
35+
```powershell
36+
winget install agentty # or: scoop install agentty
37+
```
38+
39+
The winget manifest lives in `packaging/winget/`; the `publish-winget` job in
40+
the release workflow opens a PR to `microsoft/winget-pkgs` automatically once
41+
you add a `WINGET_TOKEN` secret (a classic PAT with `public_repo` scope). Zero
42+
cost, zero Azure.
43+
44+
### Option B (any CA, simplest paid path): a PFX certificate
45+
46+
Buy a code-signing certificate from **any** CA — SSL.com, Certum, DigiCert,
47+
GlobalSign (~\$200–600/yr). Export it as a `.pfx`, base64-encode it, and add two
48+
GitHub secrets:
49+
50+
| Secret | Value |
51+
|--------|-------|
52+
| `WINDOWS_CERT_BASE64` | `base64 -w0 cert.pfx` output |
53+
| `WINDOWS_CERT_PASSWORD` | the PFX password |
54+
55+
`build-msi.ps1` signs both the `.exe` and `.msi` with it. An **OV** cert clears
56+
SmartScreen after it builds download reputation; an **EV** cert clears it
57+
immediately (but EV keys live on a hardware token and can't be exported — use
58+
the vendor's cloud signer or Option C for those).
59+
60+
> Note: as of 2023, publicly-trusted keys must live on FIPS-140-2 hardware. A
61+
> plain exported `.pfx` still works for **OV** certs from CAs that issue them in
62+
> software; for EV, use a cloud signing service.
63+
64+
### Option C (cloud HSM): Azure Trusted Signing
65+
66+
If you'd rather not handle a token, Azure Trusted Signing (~\$10/mo) keeps the
67+
key in Microsoft's HSM and clears SmartScreen immediately. Set
68+
`TRUSTED_SIGNING_ENDPOINT`, `TRUSTED_SIGNING_ACCOUNT`, `TRUSTED_SIGNING_PROFILE`
69+
(+ the `AZURE_*` service-principal secrets). `build-msi.ps1` auto-detects this
70+
backend.
71+
72+
**With none of the above configured, CI still publishes a valid UNSIGNED MSI**
73+
— which already gives PATH + Start Menu + clean uninstall, a big step up from a
74+
raw `.exe`. Most users will install via winget/Scoop anyway and never see a
75+
warning.
5576

5677
## Files
5778

packaging/windows/build-msi.ps1

Lines changed: 58 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -79,43 +79,69 @@ if (-not (Get-Command wix -ErrorAction SilentlyContinue)) {
7979
# UI extension provides WixUI_InstallDir
8080
wix extension add -g WixToolset.UI.wixext | Out-Null
8181

82-
# ---- (optional) sign the exe BEFORE packaging --------------------------------
83-
function Invoke-TrustedSign($file) {
82+
# ---- (optional) code-sign a file --------------------------------------------
83+
# Supports TWO signing backends, no Azure required:
84+
# 1. A generic certificate (any CA — SSL.com, Certum, DigiCert, …) supplied as
85+
# a base64-encoded PFX in $env:WINDOWS_CERT_BASE64 + $env:WINDOWS_CERT_PASSWORD.
86+
# This is the simplest path: buy a code-signing cert from any vendor, export
87+
# it as .pfx, base64 it, drop it in a GitHub secret. (EV certs on hardware
88+
# tokens can't be exported — use the Azure path or the vendor's cloud signer.)
89+
# 2. Azure Trusted Signing, if $env:TRUSTED_SIGNING_ENDPOINT is set.
90+
# If neither is configured, the file is left unsigned (still a valid MSI).
91+
function Get-SignTool {
92+
$st = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" -ErrorAction SilentlyContinue |
93+
Sort-Object FullName | Select-Object -Last 1
94+
if (-not $st) { throw "signtool.exe not found (install the Windows SDK)" }
95+
return $st.FullName
96+
}
97+
98+
function Invoke-Sign($file) {
8499
if (-not $Sign) { return }
85-
if (-not $env:TRUSTED_SIGNING_ENDPOINT) {
86-
Warn "signing requested but TRUSTED_SIGNING_* env not set — leaving $file unsigned"
100+
101+
# --- backend 1: generic PFX certificate (any CA) ---
102+
if ($env:WINDOWS_CERT_BASE64) {
103+
Info "code-signing $file with PFX certificate"
104+
$pfx = Join-Path $env:TEMP "codesign.pfx"
105+
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_BASE64))
106+
$signtool = Get-SignTool
107+
& $signtool sign /v /fd SHA256 `
108+
/f $pfx /p "$env:WINDOWS_CERT_PASSWORD" `
109+
/tr "http://timestamp.digicert.com" /td SHA256 `
110+
$file
111+
Remove-Item $pfx -Force -ErrorAction SilentlyContinue
112+
if ($LASTEXITCODE -ne 0) { throw "signtool failed on $file" }
113+
Ok "signed $file"
87114
return
88115
}
89-
Info "code-signing $file via Azure Trusted Signing"
90-
91-
# Trusted Signing dlib + metadata describe the cert profile to signtool.
92-
$dlibDir = Join-Path $env:TEMP "tsdlib"
93-
if (-not (Test-Path "$dlibDir\bin\x64\Azure.CodeSigning.Dlib.dll")) {
94-
New-Item -ItemType Directory -Force -Path $dlibDir | Out-Null
95-
Info "fetching Microsoft.Trusted.Signing.Client"
96-
nuget install Microsoft.Trusted.Signing.Client -Version 1.0.60 `
97-
-OutputDirectory $dlibDir -ExcludeVersion | Out-Null
116+
117+
# --- backend 2: Azure Trusted Signing ---
118+
if ($env:TRUSTED_SIGNING_ENDPOINT) {
119+
Info "code-signing $file via Azure Trusted Signing"
120+
$dlibDir = Join-Path $env:TEMP "tsdlib"
121+
if (-not (Test-Path "$dlibDir\bin\x64\Azure.CodeSigning.Dlib.dll")) {
122+
New-Item -ItemType Directory -Force -Path $dlibDir | Out-Null
123+
nuget install Microsoft.Trusted.Signing.Client -Version 1.0.60 `
124+
-OutputDirectory $dlibDir -ExcludeVersion | Out-Null
125+
}
126+
$dlib = Get-ChildItem -Recurse $dlibDir -Filter "Azure.CodeSigning.Dlib.dll" | Select-Object -First 1
127+
$meta = Join-Path $env:TEMP "metadata.json"
128+
@{
129+
Endpoint = $env:TRUSTED_SIGNING_ENDPOINT
130+
CodeSigningAccountName = $env:TRUSTED_SIGNING_ACCOUNT
131+
CertificateProfileName = $env:TRUSTED_SIGNING_PROFILE
132+
} | ConvertTo-Json | Set-Content -Path $meta -Encoding ASCII
133+
$signtool = Get-SignTool
134+
& $signtool sign /v /fd SHA256 /tr "http://timestamp.acs.microsoft.com" /td SHA256 `
135+
/dlib $dlib.FullName /dmdf $meta $file
136+
if ($LASTEXITCODE -ne 0) { throw "signtool failed on $file" }
137+
Ok "signed $file"
138+
return
98139
}
99-
$dlib = Get-ChildItem -Recurse $dlibDir -Filter "Azure.CodeSigning.Dlib.dll" |
100-
Select-Object -First 1
101-
$meta = Join-Path $env:TEMP "metadata.json"
102-
@{
103-
Endpoint = $env:TRUSTED_SIGNING_ENDPOINT
104-
CodeSigningAccountName = $env:TRUSTED_SIGNING_ACCOUNT
105-
CertificateProfileName = $env:TRUSTED_SIGNING_PROFILE
106-
} | ConvertTo-Json | Set-Content -Path $meta -Encoding ASCII
107-
108-
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" |
109-
Sort-Object FullName | Select-Object -Last 1
110-
& $signtool.FullName sign `
111-
/v /fd SHA256 /tr "http://timestamp.acs.microsoft.com" /td SHA256 `
112-
/dlib $dlib.FullName /dmdf $meta `
113-
$file
114-
if ($LASTEXITCODE -ne 0) { throw "signtool failed on $file" }
115-
Ok "signed $file"
140+
141+
Warn "signing requested but no certificate configured — leaving $file unsigned"
116142
}
117143

118-
Invoke-TrustedSign $Exe
144+
Invoke-Sign $Exe
119145

120146
# ---- build the MSI -----------------------------------------------------------
121147
Info "building $msi (WiX v4, $Arch)"
@@ -131,6 +157,6 @@ if ($LASTEXITCODE -ne 0) { throw "wix build failed" }
131157
Ok "built $msi"
132158

133159
# ---- sign the MSI ------------------------------------------------------------
134-
Invoke-TrustedSign $msi
160+
Invoke-Sign $msi
135161

136162
Ok "done: $msi"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
2+
PackageIdentifier: agentty.agentty
3+
PackageVersion: "0.1.0"
4+
Platform:
5+
- Windows.Desktop
6+
MinimumOSVersion: 10.0.0.0
7+
InstallModes:
8+
- interactive
9+
- silent
10+
- silentWithProgress
11+
UpgradeBehavior: install
12+
# The InstallerSha256 is filled in per release by CI (compute over the .msi).
13+
Installers:
14+
- Architecture: x64
15+
InstallerType: wix
16+
InstallerUrl: https://github.com/1ay1/agentty/releases/download/v0.1.0/agentty-windows-x86_64.msi
17+
InstallerSha256: REPLACE_WITH_MSI_SHA256
18+
ProductCode: "{6F3A2C18-9E4B-4D27-8A1E-7C5B2D0F9A41}"
19+
ManifestType: installer
20+
ManifestVersion: 1.6.0
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
2+
PackageIdentifier: agentty.agentty
3+
PackageVersion: "0.1.0"
4+
PackageLocale: en-US
5+
Publisher: agentty
6+
PublisherUrl: https://agentty.org
7+
PublisherSupportUrl: https://github.com/1ay1/agentty/issues
8+
PackageName: agentty
9+
PackageUrl: https://agentty.org
10+
License: MIT
11+
LicenseUrl: https://github.com/1ay1/agentty/blob/master/LICENSE
12+
Copyright: Copyright (c) agentty contributors
13+
ShortDescription: Blazing-fast Claude in your terminal — a C++26 alternative to claude-code.
14+
Description: >-
15+
agentty is a native C++26 terminal coding agent and a drop-in alternative to
16+
claude-code. A single static binary with sub-millisecond cold start, sandboxed
17+
by default, and one-command SSH air-gap. No Node, Python, or Electron. Signs in
18+
with your Claude Pro/Max subscription or an ANTHROPIC_API_KEY.
19+
Moniker: agentty
20+
Tags:
21+
- ai
22+
- claude
23+
- cli
24+
- coding-agent
25+
- terminal
26+
- developer-tools
27+
ReleaseNotesUrl: https://github.com/1ay1/agentty/releases/latest
28+
ManifestType: defaultLocale
29+
ManifestVersion: 1.6.0
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json
2+
# Submitted to microsoft/winget-pkgs as manifests/a/agentty/agentty/<version>/
3+
PackageIdentifier: agentty.agentty
4+
PackageVersion: "0.1.0"
5+
DefaultLocale: en-US
6+
ManifestType: version
7+
ManifestVersion: 1.6.0

0 commit comments

Comments
 (0)