Stop Hand-Rolling Chocolate

Automating Chocolatey with psake

Gilbert Sanchez

@HeyItsGilbert

Thanks

Hey! It's Gilbert

Hands Up ✋

"Back in my day..."

The year is 2013

The Build Process

"The ticket queue was the build process."

  • Teams needed a Windows packages built and deployed.
  • They filed a ticket
  • Someone (eventually) hand-crafted it
  • That someone was the only one who knew how

"It Works on My Machine"

Except your devs are on Macs

  • File locking differently
  • Windows-specific paths, encodings, behaviors
  • Packaging Windows software felt like a foreign language

Extra friction became an extra excuse.

Chocolatey is just PowerShell

If you can write a script, you can ship a package.

Scaffolding a Package

choco new mypackage

mypackage/
├── mypackage.nuspec           ← package metadata
└── tools/
    ├── chocolateyInstall.ps1  ← runs on install
    ├── chocolateyUninstall.ps1
    └── LICENSE.txt

That's the whole package.

The Two Files That Matter: Metadata

mypackage.nuspecwhat the package is

<metadata>
  <id>mypackage</id>
  <version>1.0.0</version>
  <description>Does a thing.</description>
</metadata>

The Two Files That Matter: Installer

tools/chocolateyInstall.ps1how it installs

$packageArgs = @{
  packageName = 'mypackage'
  url         = 'https://example.com/installer.exe'
  checksum    = 'ABC123...'
}
Install-ChocolateyPackage @packageArgs

Extensions

Add new functions to any package

mycompany-chocolatey-extension/
└── extensions/
    └── mycompany-helpers.psm1  ← imported automatically

Hooks

Run logic around every install

mycompany-hooks/
└── hooks/
    ├── pre-install-all.ps1     ← before any package installs
    └── post-install-all.ps1    ← after any package installs

psake

Tasks with dependencies

That's it. PowerShell Tasks.

Your Build Process is a README and Hope


# HOW TO RELEASE (don't forget these steps)
1. Validate the nuspec manually
2. Run choco pack
3. Run the tests (yes, before you push JOHN!)
4. Push to the feed — but only on main!

Every step is a chance for someone to skip it.

Manual Steps → Declared Tasks

Task Default -depends Pack

Task Test -depends -description "Run Pester tests" {
    Invoke-Pester .\Tests\
}

Task Pack -depends Test -description "Build the .nupkg" {
    exec { choco pack }
}

Task Push -depends Pack -precondition { $env:GITHUB_REF -eq 'refs/heads/main' } {
    exec { choco push mypackage.nupkg --source $env:CHOCO_FEED }
}

The Task Graph

Push  (only on main)
 └── Pack
      └── Test

Run everything: Invoke-psake

Run just tests: Invoke-psake -taskList Test


Same command. Local or CI. No surprises.

The Brazil Story

The Brazil Story

Extensions put your org's knowledge in one place.

# mycompany-chocolatey-extension/extensions/mycompany-helpers.psm1
function Get-PackageFromCDN {
    param($PackageName, $Version)

    $node   = Resolve-NearestCDNNode          # ← finds São Paulo, not Virginia
    $cached = Test-CDNCache -Node $node -Package $PackageName

    if ($cached) { Get-FromCache  -Node $node -Package $PackageName }
    else         { Get-FromOrigin -Package $PackageName -Version $Version }
}

Every package calls Get-PackageFromCDN. Nobody hard-codes the CDN.

Let's build it


Blank repo → published, tested, CI-backed package.

What We Just Built

Step What it does
.\build.ps1 -Task Test Runs Pester tests against the package
.\build.ps1 -Task Pack Builds the .nupkg
.\build.ps1 -Task Push Publishes — but only from main
CI workflow Validates + tests every PR
Publish workflow Auto-publishes on merge

Same psakefile. Local and CI. No surprises.

"I Only Have 5 Packages"

You still get:

  • Auto-publish on merge
  • Validated nuspec XML
  • Pester tests
  • Extensions & Hooks

Automation that reduces toil scales down just as well as it scales up.

Your Mac Dev Just Shipped a Windows Package


They didn't touch a Windows machine.

They didn't learn Chocolatey internals.

They opened a PR. CI went green. They merged.


The packaging knowledge lives in the psakefile — not in someone's head.

Boring is good

Predictable. Auditable. Repeatable.

The best build pipeline is the one you forget about.

THANK YOU

Feedback is a gift

Please review this session via the mobile app

Questions? Find me @heyitsgilbert

2pm Monday

Author slide

Speaker notes: Welcome everyone. Quick show of hands — who here manages Chocolatey packages? Who's done it by hand? Keep that hand up if you've ever said "just run this script and then run that one." Yeah. That's what we're fixing today.

Panning shot to a young and eager Gilbert ripsticking down the hallway.... Responsible for lots of things. Patching, vmware, etc.

Speaker notes: At Meta we had hundreds of apps running across 30,000 Windows machines. The "build process" for Chocolatey packages was: file a ticket, wait, hope. The person who built your package kept the knowledge in their head. Then they left. Or got pulled onto something else. And you were stuck. That doesn't scale. It barely works at 10 packages.

Speaker notes: This was a real pattern. Teams building cross-platform software would just skip Windows packaging because it felt hard and alien. The file locking bugs alone were a constant source of tickets. POSIX vs Windows file locking semantics bite you in ways you don't expect until a machine at 2am tells you about it. If the tooling makes it hard, people won't do it. Simple as that.

Speaker notes: This is the core insight I want you to leave with, even if you forget everything else. There's no magic. No proprietary format. No arcane knowledge required. A Chocolatey package is a nuspec file and a PowerShell script. That's it. We're going to prove it in about 10 minutes. Side note for consumers: choco install mypackage is all they ever see. Your packaging work is invisible to them — which is the goal.

Act 2: Chocolatey Crash Course

7m mark - 2:07p choco new scaffolds everything for you. The nuspec is your package manifest — name, version, dependencies. The install script is PowerShell that runs when someone does choco install. Two files that matter. That's the whole mental model.

The nuspec is just XML. The install script is just PowerShell.

Install-ChocolateyPackage handles downloads, checksums, and silent installs. If you know PowerShell, you already know how to write this. No Chocolatey-specific magic. Just PowerShell with convenience functions on top.

Speaker notes: <pre|post>-<install|beforemodify|uninstall>-<packageID>.ps1 Hooks fire around every single install without the package author doing anything. Install your hook package once, and it runs everywhere. We'll come back to this with a real example in a minute.

13-15m mark ~ 2:15pm Speaker notes: psake — pronounced "sake" like the Japanese rice wine, not "p-sake" — is a build automation tool written in PowerShell. Think Make or Rake, but for PowerShell people. You declare tasks, you declare what each task depends on, psake runs them in the right order and stops loudly if something fails. I maintain psake, which is either a great reason to trust me on this or a great reason to be suspicious. I'll let you decide. The reason I keep coming back to it for Chocolatey work is the same reason I fell for it in the first place: it's the language you're already writing. No context switch. No new mental model. Just PowerShell.

Show of hands — who has a README like this? The problem isn't the steps. The steps are fine. The problem is that the steps live in a document nobody reads until something breaks. That's not a people problem. That's an automation problem.

Every "don't forget" becomes a dependency. The precondition on Push means it simply won't run unless you're on main — no human judgement required. Notice the descriptions — that's what shows up in Invoke-psake -docs. Your build script is now self-documenting. Sarah doesn't need to read the README.

The dependency graph means you never think about order again. Want to just repack without re-running tests? Invoke-psake -taskList Pack. The exact same command you just ran at your desk is what GitHub Actions will run. No drift. If it passes locally, it passes in CI. Now — let me show you why this matters when your infrastructure is more complicated than a single package. The Brazil story.

Small office. Bad connection. 200mb Office Killed. CDN was in DC's.

We put all of that CDN routing logic into an extension. One module. One place to fix. One place to improve. Teams writing packages didn't need to know any of it existed. They called Get-PackageFromCDN and it did the right thing for their location. That's what extensions are for: your org's infrastructure knowledge in one module, not scattered across hundreds of install scripts.

Act 4: Live Demo

25-30m - 2:30 Here's what we're going to build in the next 20 minutes: A GitHub repo with a Chocolatey package, a psakefile that validates, tests, and packs it, and GitHub Actions workflows that run CI on PRs and auto-publish when we merge to main. If you want to follow along, the repo will be linked at the end. [SWITCH TO TERMINAL] Demo script: 1. gh repo create choco-psake-demo --public 2. choco new mypackage — show the generated structure 3. Write psakefile.ps1 with Test, Pack, Push tasks 4. Add nuspec XML schema validation task 5. Add Pester test for package metadata 6. Run Invoke-psake locally — watch it pass 7. Add .github/workflows/ci.yml — test on PR 8. Add .github/workflows/publish.yml — push on merge to main 9. Push, open a PR, watch CI go green 10. Merge, watch the package publish

Act 5: Payoff

Goal: 40m Speaker notes: Here's what we just wired together. The key insight is the bottom row — none of this required separate scripts for local vs CI. One psakefile. Everywhere.

Speaker notes: I want to kill a misconception before you leave. This is not enterprise-only tooling. The patterns we used today — declared tasks, validated XML, tested packages, auto-publishing — these pay off at 5 packages just as much as 500. You get your Saturday afternoons back. You stop being the person who has to manually push a release because nobody else knows how. That's worth it at any scale. - Auto-publish on merge — no manual push steps - Validated nuspec XML — catch errors before they ship - Pester tests — know it works before your users find out it doesn't - Extensions — your org's logic in one place, not copy-pasted everywhere - Hooks — compliance, logging, CDN routing — free for every package

Speaker notes: This is the Meta story, but it's also your story if you want it to be. The developer who doesn't "do Windows" can still contribute because the process is encoded, tested, and automated. The person who built the psakefile doesn't need to be in the loop for every release. That's what good tooling does — it transfers knowledge from people into systems.

Speaker notes: I want to leave you with this. The goal was never "cool automation." The goal was never to impress anyone with your psakefile. The goal is a build pipeline so reliable, so boring, that you stop thinking about it entirely — and just ship packages. That's what we built today. Demo repo is at the link. psake is open source and always looking for contributors. I'm around for questions — thank you.