agentsclimarketplace

Windows release pipeline

Skill NerdBase-by-Stark/claude-code-skills/windows-release-pipeline

GitHub Actions CI/CD for Python/PyInstaller Windows desktop apps — matrix builds, Azure Trusted Signing via OIDC, Release Drafter, dependency caching, artifact attestation. Use when setting up or modifying CI/CD for a Python Windows .exe release pipeline.From its SKILL.md

Install
npx -y skills add NerdBase-by-Stark/claude-code-skills --skill windows-release-pipeline

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • reads credentialsReads from 6 credential sources: `secrets.AZURE_TENANT_ID` and 5 more.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 7 commands, including `python -m pip install --upgrade pip` and 6 more.

SKILL.md

13.7 KB, ~3.4k tokens by cl100k_base, as published. Nobody here has run it

Windows Release Pipeline — Python/PyInstaller Desktop Apps

Production patterns for automating .exe builds, signing, and GitHub releases for Python desktop apps on Windows. Scope: PyInstaller + PySide6 + GitHub Actions. Verified against 2026 runner images and action maintenance status.

Pairs with pyside6-desktop (PyInstaller rules, code signing fundamentals) — this skill covers the automation layer.


1. Reference Workflow (copy-paste starter)

Triggered by tag push (v0.6.0, v1.0.0, etc.). Builds, signs via Azure Trusted Signing, verifies, and publishes the release.

name: Release

on:
  push:
    tags: ['v*']

permissions:
  contents: write       # create releases
  id-token: write       # OIDC for Azure Trusted Signing
  attestations: write   # SBOM / artifact attestations (optional)

jobs:
  build:
    runs-on: windows-latest
    strategy:
      matrix:
        python-version: ['3.11', '3.12']

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0           # full history for changelog

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: 'pip'
          cache-dependency-path: 'requirements.txt'

      - name: Install deps
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install "PyInstaller==6.11.0"   # pinned — see Rule 3

      - name: Verify .spec is committed
        shell: cmd
        run: if not exist "gude-deploy.spec" exit /b 1

      - name: Build
        run: pyinstaller gude-deploy.spec --distpath ./dist

      - name: Sign via Azure Trusted Signing
        uses: azure/trusted-signing-action@v0
        with:
          azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
          azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
          endpoint: ${{ secrets.AZURE_CODESIGNING_ENDPOINT }}
          trusted-signing-account-name: ${{ secrets.AZURE_CODESIGNING_ACCOUNT }}
          certificate-profile-name: ${{ secrets.AZURE_CODESIGNING_CERT_PROFILE }}
          files-folder: ./dist

      - name: Verify signed
        shell: cmd
        run: signtool verify /pa /all .\dist\GudeDeploy\GudeDeploy.exe

      - name: Package
        shell: pwsh
        run: Compress-Archive -Path dist/GudeDeploy -DestinationPath "dist/gude-deploy-${{ github.ref_name }}-py${{ matrix.python-version }}.zip"

      - uses: actions/upload-artifact@v4
        with:
          name: builds-py${{ matrix.python-version }}
          path: dist/gude-deploy-*.zip

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4

      - uses: ncipollo/release-action@v1
        with:
          artifacts: 'builds-*/*.zip'
          generateReleaseNotes: true
          draft: false

2. Rules

Rule 1: Commit the .spec file; never regenerate in CI

PyInstaller .spec files encode pathex, hiddenimports, and hooks. Regenerating in CI captures the runner's environment, not yours — wrong paths, missing hidden imports. The .spec is source code; treat it as such.

# WRONG — regenerates spec in CI, ignores your local tuning
- run: pyi-makespec gude-deploy.py

# RIGHT — use committed .spec
- run: pyinstaller gude-deploy.spec

Add a guard step: fail the build if the spec file is missing.

Rule 2: Enable setup-python dependency caching — saves ~3 min on a PySide6 build

PySide6 alone is ~60 MB. On a cold pip install, it's the dominant cost. actions/setup-python@v5 caches pip downloads keyed on your requirements.txt hash:

- uses: actions/setup-python@v5
  with:
    python-version: '3.11'
    cache: 'pip'
    cache-dependency-path: 'requirements.txt'

Cache hit → skip the ~3 min PySide6 download. Cache invalidates automatically on requirements.txt change.

Don't cache build/ or dist/ — PyInstaller's own build is I/O-bound, not CPU-bound, and caching multi-MB artifacts wastes GitHub's 10 GB cache quota for marginal gain.

Rule 3: Pin PyInstaller version; bump deliberately

PyInstaller 6.x has had multiple Windows-specific regressions (setuptools hook missing in 6.0–6.6, "Looking for dynamic library" hang in 6.5, various PySide6 hook misses). Pin the version so CI doesn't silently upgrade overnight:

# requirements.txt
PyInstaller==6.11.0

When bumping: build locally first, test the frozen .exe, commit both the version bump and (if needed) any new hiddenimports in the .spec. Never pin PyInstaller>=6.0 or leave it unpinned.

Rule 4: Prefer Azure Trusted Signing + OIDC over PFX-in-secrets

Two reasons to avoid base64-encoded PFX files stored as GitHub Secrets:

  1. Attack surface. Leaked logs or compromised workflow dumps the cert material.
  2. CA/Browser Forum rules (June 2023+). Private keys must live on FIPS 140-2 Level 2+ hardware. Storing an exportable key as a file is non-compliant.

Azure Trusted Signing via OIDC federation avoids both. No secrets in repo — federated identity, cloud HSM handles the keys. Runs on stock windows-latest, no self-hosted runner needed.

If you're stuck with hardware tokens (YubiKey / eToken) instead, you must use a self-hosted runner with the token attached. GitHub-hosted runners can't see USB devices. See pyside6-desktop Rule 40 for the cost/complexity comparison.

Rule 5: Always verify signature in CI — never trust the signing step's exit code alone

The signing action can exit 0 while producing an invalid signature (expired cert, bad timestamp server at sign time, wrong cert profile). Always follow signing with an explicit signtool verify:

- name: Verify signed
  shell: cmd
  run: signtool verify /pa /all .\dist\GudeDeploy\GudeDeploy.exe

/pa = "Default Authentication Verification Policy" — the right flag for end-user distribution. Failing exit code aborts the job before release publication, preventing unsigned artifacts from reaching customers.

Rule 6: Use Release Drafter with PR labels, not Conventional Commits

If your project doesn't retrofit Conventional Commits (feat:, fix:, chore: prefixes), most changelog actions will produce empty or broken release notes.

Release Drafter works from PR labels instead:

# .github/release-drafter.yml
categories:
  - title: '🚀 Features'
    labels: ['feature', 'enhancement']
  - title: '🐛 Bug Fixes'
    labels: ['fix', 'bugfix']
  - title: '📖 Documentation'
    labels: ['docs']
template: |
  ## Changes
  $CHANGES

Works with any commit history. Pair with branch protection that requires a label on every PR merge. Simpler alternative: use GitHub's built-in generateReleaseNotes: true in ncipollo/release-action — it parses PR titles automatically.

Rule 7: ncipollo/release-action > softprops/action-gh-release in 2026

softprops/action-gh-release still works but has accumulated unaddressed issues (#445, #451 as of April 2026). ncipollo/release-action@v1 is actively maintained, supports generateReleaseNotes: true, updateOnlyUnreleased, and file globs:

- uses: ncipollo/release-action@v1
  with:
    artifacts: 'builds-*/*.zip'
    generateReleaseNotes: true
    draft: false

Don't be the team that notices their release action died when it's time to ship.

Rule 8: Separate build and release jobs across OS runners

Run build on windows-latest (needed for Windows builds). Run release on ubuntu-latest — it's faster to spin up and just needs to glue zip files to a GitHub release. Using one Windows job for both wastes 30-60s per release on the slower runner boot.

build:
  runs-on: windows-latest
  # ...
release:
  needs: build          # ← sequential dependency
  runs-on: ubuntu-latest

Rule 9: Known PyInstaller 6.x Windows CI traps

Two specific breakages to pre-guard against:

  • setuptools≥70 + PyInstaller 5.13–6.6: pkg_resources hook missing. Add to .spec:

    hiddenimports=['pkg_resources.extern']
    

    Fixed in PyInstaller 6.7+. (Discussion #7490)

  • "Looking for dynamic library" hangs on windows-latest: Intermittent on PyInstaller 6.5+. Reproduce locally with the same Python patch version before pushing to catch it pre-CI. (Issue #8396)

Rule 10: Skip SBOM / artifact attestation for v1 unless you need supply-chain compliance

anchore/sbom-action + actions/attest-sbom produce software bills of materials and cryptographic attestations. They're valuable for:

  • Regulated industries (healthcare, defense, finance)
  • Supply-chain audit requirements (SLSA Level 3+)
  • Multi-tenant open-source distribution

For a private tool shipped to one customer, skip it until someone asks. It adds workflow complexity and SBOM noise without customer-visible benefit.

Revisit if/when an enterprise customer asks for SLSA provenance.


3. Action / Tool Recommendation Matrix (verified April 2026)

PurposeToolStatusUse
Python setup + pip cacheactions/setup-python@v5Active (Microsoft)Always
Sign .exe (cloud HSM)azure/trusted-signing-action@v0Active (Microsoft)Recommended default
Sign .exe (hardware token)AzureSignTool (self-hosted)ActiveIf stuck with YubiKey/eToken
Sign .exe (cross-platform)osslsigncodeActiveLinux/macOS CI with PKCS#11
Create releasencipollo/release-action@v1ActiveRecommended
Create release (older alt)softprops/action-gh-release@v2StaleSkip for new workflows
Draft release notesrelease-drafter/release-drafter@v6ActiveAlt to generateReleaseNotes
Generate SBOManchore/sbom-action@v0ActiveOnly for compliance needs
Attest artifactactions/attest-build-provenance@v1Beta (GitHub)Only for compliance needs
Windows ARM64 runnerruns-on: windows-11-arm (public repos)GA April 2025If shipping ARM64 .exe

4. Common anti-patterns

  1. Regenerating .spec in CI — captures runner env, breaks reproducibility
  2. Caching dist/ — wastes GitHub cache quota, doesn't speed up rebuilds
  3. Unpinned PyInstaller — silent breakage on every new release
  4. PFX base64 in GitHub Secrets — attack surface + CA/B Forum non-compliance
  5. Skipping signature verification — unsigned builds ship when signing silently fails
  6. Trying to build Windows ARM64 on windows-latest — cross-compile doesn't work; use windows-11-arm runner
  7. Running release job on windows-latest — 30-60s slower than ubuntu-latest for what's just a GitHub API call

5. Secrets layout (recommended)

Keep these in repo → Settings → Secrets and variables → Actions:

SecretPurposeHow to obtain
AZURE_TENANT_IDOIDC federation targetAzure portal → Entra ID
AZURE_CLIENT_IDFederated app identityAzure portal → App registrations
AZURE_CLIENT_SECRET(or rely on OIDC, not static)Only if not using OIDC
AZURE_CODESIGNING_ENDPOINTSigning endpoint URLTrusted Signing resource page
AZURE_CODESIGNING_ACCOUNTSigning account nameTrusted Signing resource page
AZURE_CODESIGNING_CERT_PROFILECertificate profileTrusted Signing → Profiles

OIDC federation replaces AZURE_CLIENT_SECRET entirely — safer. Set up via Microsoft Learn: OIDC for GitHub Actions.


6. Open questions (verify before adopting)

  • Windows ARM64 cross-compile on windows-latest: Not currently supported; need a separate windows-11-arm runner job. Verify if PySide6 ARM64 wheels exist for your Python version before enabling.
  • Smart App Control compatibility: Windows 11 Smart App Control may reject even signed PyInstaller builds depending on reputation. Test post-signing on a fresh Win 11 image before customer rollout.
  • Release Drafter tag-triggered vs PR-triggered: Release Drafter drafts on every PR merge by default. For tag-triggered releases, ensure the drafter job runs separately and the release job finalizes the draft.

Sources

What ships with it: 1 file

2.2 KB alongside SKILL.md

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.