PSI WinGet Source

Private WinGet package source for distributing PSI internal developer tools. Built with .NET 8 Minimal APIs, deployed to Azure App Service.


Quick Start

The PSI WinGet source is deployed to all managed machines via Intune policy. If it’s not on your machine yet:

# Check if PSI source is registered
winget source list
 
# If not listed, an admin can register it:
winget source add --name PSI --arg https://packages.progressivesurface.com/api --type Microsoft.Rest --accept-source-agreements

Install a package:

winget install PSI.CSM

Search for packages:

winget search --source PSI PSI

Check for updates:

winget upgrade --source PSI

Available Packages

Package IDNameDescription
PSI.CSMClaude Session ManagerTUI for browsing and managing Claude Code sessions

Architecture

winget install PSI.CSM
    │
    │  HTTPS (private endpoint)
    │
    └──► ps-winget-source (.NET 8 Minimal API)
         │
         ├── /api/information          ← WinGet REST endpoints
         ├── /api/packages
         ├── /api/packageManifests/{id}
         ├── /api/manifestSearch
         ├── /api/download/{id}        ← Proxied installer downloads
         │
         ├── /api/admin/packages       ← Admin API
         │
         ├── SQLite (package metadata)
         └── Azure Blob (psiwingetpkgs → installer binaries)

Components

ComponentDetails
API.NET 8 Minimal API (psi-winget-source)
App Serviceps-winget-source on asp-erp-migration-tool plan (PS-WEBAPPS)
DatabaseSQLite (EF Core), persistent on /home/winget.db
Installer Storagepsiwingetpkgs Azure Blob Storage, container installers (private)
Container Registrypsicontainers.azurecr.io
Domainpackages.progressivesurface.com (private endpoint: 10.160.140.17)
SSLWildcard cert *.progressivesurface.com
Entra AppPSI WinGet Source (b0b01ac6-8f1f-4b55-af50-c582da3dfd77)

How Downloads Work

Installer binaries are stored in private Azure Blob Storage. The WinGet client never accesses blob storage directly. Instead:

  1. WinGet requests the package manifest from /api/packageManifests/{id}
  2. The manifest contains an installer URL pointing to /api/download/{installerId}
  3. WinGet downloads from that URL
  4. The API streams the file from blob storage to the client

This keeps blob storage completely private with no public access or SAS tokens.


Intune Deployment

The PSI WinGet source is deployed to all managed machines via Intune Settings Catalog (not scripts).

Settings Catalog Configuration

Profile: Devices → Configuration → Settings Catalog

SettingValue
Enable App InstallerEnabled
Enable App Installer Additional SourcesEnabled

Additional Sources entry:

{"Name":"PSI","Arg":"https://packages.progressivesurface.com/api","Data":"","Explicit":false,"Identifier":"PSI","Type":"Microsoft.Rest","TrustLevel":["Trusted"]}

This writes to HKLM\Software\Policies\Microsoft\Windows\AppInstaller via MDM/CSP. The source is available to all users on the machine and cannot be removed by users.

Do NOT use EnableAllowedSources unless you intend to restrict which sources users can add. It acts as a whitelist and will block sources not in the list.


CI/CD

Push to main in the psi-winget-source repo triggers automatic deployment:

  1. GitHub Actions runs on ps-cicd-runner (self-hosted Linux)
  2. az acr build — remote Docker build in Azure Container Registry
  3. App Service is configured to pull the new image from psicontainers.azurecr.io
  4. App Service restarts with the new container

No Docker required on the CI runner — ACR builds remotely.


Admin API

Manage packages via REST API. Swagger UI available at the root URL (packages.progressivesurface.com).

Create a Package

curl -X POST https://packages.progressivesurface.com/api/admin/packages \
  -H "Content-Type: application/json" \
  -d '{"Identifier":"PSI.MyTool","Name":"My Tool","Publisher":"Progressive Surface Inc"}'

Add a Version

curl -X POST https://packages.progressivesurface.com/api/admin/packages/PSI.MyTool/versions \
  -H "Content-Type: application/json" \
  -d '{
    "Version":"1.0.0",
    "ShortDescription":"What this tool does",
    "InstallerUrl":"https://psiwingetpkgs.blob.core.windows.net/installers/PSI.MyTool/1.0.0/mytool-1.0.0-win-x64.zip",
    "InstallerSha256":"<sha256>",
    "Architecture":"x64",
    "InstallerType":"zip",
    "NestedInstallerType":"portable",
    "PortableCommandAlias":"mytool"
  }'

List All Packages

curl https://packages.progressivesurface.com/api/admin/packages

Delete a Version

curl -X DELETE https://packages.progressivesurface.com/api/admin/packages/PSI.MyTool/versions/1.0.0

Publishing a New Package

1. Build the installer

Python tools:

pip install pyinstaller
pyinstaller --onefile --name mytool mytool.py
# Creates dist/mytool.exe

.NET tools:

dotnet publish -c Release -r win-x64 --self-contained

2. Create a zip and compute SHA256

Compress-Archive -Path dist/mytool.exe -DestinationPath mytool-1.0.0-win-x64.zip
(Get-FileHash mytool-1.0.0-win-x64.zip -Algorithm SHA256).Hash

3. Upload to blob storage

az storage blob upload --account-name psiwingetpkgs --container-name installers \
  --file mytool-1.0.0-win-x64.zip \
  --name "PSI.MyTool/1.0.0/mytool-1.0.0-win-x64.zip" \
  --auth-mode key

4. Register via admin API

Use the admin API calls above to create the package and add the version with the blob URL and SHA256.

5. Test

winget search --source PSI mytool
winget install PSI.MyTool --source PSI

Project Structure

psi-winget-source/
├── src/PSI.WinGet.API/
│   ├── Program.cs              # Startup, middleware chain
│   ├── Endpoints/
│   │   ├── WinGetEndpoints.cs  # 4 WinGet REST API endpoints + download proxy
│   │   ├── AdminEndpoints.cs   # Package management CRUD
│   │   └── HealthEndpoints.cs  # /api/health
│   ├── Services/
│   │   ├── PackageService.cs   # CRUD + search + manifest generation
│   │   └── BlobStorageService.cs  # Azure Blob download proxy
│   ├── Models/                 # DB entities + WinGet response DTOs
│   └── Data/
│       └── WinGetDbContext.cs  # EF Core + SQLite
├── Dockerfile                  # Multi-stage .NET 8 build
├── scripts/intune/             # Intune detection/remediation scripts (backup)
└── .github/workflows/deploy.yml

Troubleshooting

IssueSolution
”No sources match the given value: PSI”Source not registered. Check winget source list. Register via Intune policy or manually with winget source add.
”The configured rest source is not supported”API may be down or returning wrong format. Check curl https://packages.progressivesurface.com/api/information.
Download fails (409 Conflict)Package may already be installed. Run winget uninstall PSI.CSM first.
Download fails (other)Check that the installer blob exists and the download proxy is working: curl https://packages.progressivesurface.com/api/download/1.
Package not found after publishingThe SQLite DB resets on container restart. Re-seed via admin API. (Future: persistent volume or external DB.)
Source not appearing after Intune pushRun winget source list — check “Group Policy” section in winget --info. May need a device sync in Intune.