Skip to content

Release Profiles

Picket publishes RID-specific Native AOT release archives and RID-specific Native AOT NuGet tool packages for the scanner CLI and the TUI companion. Always pass an explicit runtime identifier with -r so each archive artifact and RID tool package is tied to the target platform.

The top-level NuGet tool packages are pointer packages. The RID-specific packages carry the native executables, and the .NET CLI selects the matching package during dotnet tool install.

Terminal window
dotnet publish src/Picket.Cli/Picket.Cli.csproj -p:PublishProfile=release-speed -r win-x64
dotnet publish src/Picket.Tui.Cli/Picket.Tui.Cli.csproj -p:PublishProfile=release-speed -r win-x64
dotnet publish src/Picket.Cli/Picket.Cli.csproj -p:PublishProfile=release-minsize -r linux-x64
dotnet publish src/Picket.Tui.Cli/Picket.Tui.Cli.csproj -p:PublishProfile=release-minsize -r linux-x64
dotnet publish src/Picket.Cli/Picket.Cli.csproj -p:PublishProfile=release-diagnostics -r osx-arm64
dotnet publish src/Picket.Tui.Cli/Picket.Tui.Cli.csproj -p:PublishProfile=release-diagnostics -r osx-arm64
Profile Purpose Key settings
release-speed Default public CLI and TUI companion artifacts. Native AOT, self-contained, single-file publish, speed optimization, no public debug-symbol output, diagnostics reduced.
release-minsize Smallest supported artifact for package managers, containers, and constrained runners. Native AOT, size optimization, no public debug-symbol output, stack traces off, EventSource and metrics off, resource-key exception messages.
release-diagnostics Support artifact for bug reports and performance investigations. Native AOT, speed optimization, symbols kept, debugger support, EventSource, metrics, stack traces, and HTTP activity propagation enabled.

All profiles keep unsafe BinaryFormatter serialization, UTF-7, metadata update support, XML resolver networking by default, and HTTP/3 disabled. These switches are explicit so size and diagnostics tradeoffs are visible in review.

release-speed and release-minsize set DebugSymbols=false, DebugType=none, NativeDebugSymbols=false, and CopyOutputSymbolsToPublishDirectory=false for every RID. A post-publish target runs after the Native AOT symbol-copy target to remove residual compiler and reference symbol sidecars, and CI rejects the artifact if .pdb, .dbg, .dwarf, or .dSYM output remains. Current macOS RIDs also set StripSymbols=false until the .NET runtime pack includes the Apple Native AOT Swift module debug-info fix tracked by dotnet/runtime#123687 and dotnet/runtime#124336. That linker workaround avoids transient clang module-cache .pcm warnings without publishing debug sidecars. Use release-diagnostics when native debug information is needed.

Embeddable library packages publish .snupkg source-symbol packages. Framework-dependent tool pointer packages and RID-specific Native AOT tool packages set IncludeSymbols=false and do not publish symbol packages.

framework-dev refers to the normal framework-dependent dotnet build and dotnet run developer mode. It is not a .pubxml publish profile or a release artifact.

release-minsize must not change scanner findings, rule behavior, reports, validation states, or exit-code classification. It may reduce diagnostic richness only.

Every CI run packs the public embeddable library packages after build and test:

  • Picket.Rules
  • Picket.Engine
  • Picket.Compat
  • Picket.Report
  • Picket.Security

Every CI run also packs the Native AOT dotnet tool packages:

  • Picket, with ToolCommandName=picket
  • Picket.Tui.Cli, with ToolCommandName=picket-tui

Every solution project commits a NuGet packages.lock.json. The lock files pin resolved versions and package content hashes, including the ZstdNet package that supplies native zstandard libraries. CI, release, container, GitHub Action, and Azure Pipelines builds restore with --locked-mode, so dependency drift or a package payload mismatch fails before compilation.

The release workflow packs each tool twice: once as a top-level pointer package, and once per supported RID with dotnet pack -r <RID>. RID-specific packages are pushed to NuGet before the pointer packages so installs can resolve immediately.

Every public NuGet package, RID release archive, container image, MSI, and package-manager installation carries LICENSE, README.md where the format supports it, the package icon where applicable, and THIRD-PARTY-NOTICES.txt. The notices cover the embedded Gitleaks rule corpus, managed dependencies compiled into Native AOT binaries, Hex1b-transitive assets, Scout packages, and bundled zstandard runtimes.

The CI pack gate runs on Windows, Linux, and macOS so package metadata, root readme packaging, icon packaging, symbol packages, project-reference dependencies, tool manifests, and cross-platform MSBuild paths are validated before release automation consumes the same projects.

Every CI run also publishes picket and picket-tui with release-speed for the runner’s native RID, and the Linux jobs additionally validate the matching musl RID:

  • linux-x64 on ubuntu-latest
  • linux-arm64 on ubuntu-24.04-arm
  • linux-musl-x64 on ubuntu-latest
  • linux-musl-arm64 on ubuntu-24.04-arm
  • win-x64 on windows-latest
  • win-arm64 on windows-11-arm
  • osx-x64 on macos-26-intel
  • osx-arm64 on macos-26

This is the analyzer gate for Native AOT, trimming, single-file compatibility, and RID-specific publish behavior. A normal dotnet build is not enough evidence that the shipped executables can be produced.

The Linux jobs install musl-tools, build the decompression-only zstandard 1.5.7 runtime from its SHA-256-pinned upstream archive, and pass it to scripts/Publish-LinuxMusl.cs. The pinned archive hash matches Meta’s published zstd-1.5.7.tar.gz.sha256 release asset. The publish app builds both executables and RID-specific tool packages in a digest-pinned Alpine AOT SDK image and rejects an executable unless its ELF interpreter is musl. Picket.Cli also fails when PICKET_ZSTANDARD_MUSL_LIBRARY does not identify the verified runtime, preventing NuGet’s generic Linux asset fallback from placing a glibc library in Alpine artifacts. CI and release jobs then run the published scanner against a generated zstandard-compressed fixture in a digest-pinned .NET runtime-deps Alpine container, so a mislabeled executable, missing library, unloadable runtime, or nonfunctional decompressor fails before packaging.

For a manual musl publish on Linux:

Terminal window
dotnet run --file ./scripts/Build-ZstandardMusl.cs -- -OutputDirectory ./artifacts/zstd-musl
dotnet run --file ./scripts/Publish-LinuxMusl.cs -- -RuntimeIdentifier linux-musl-x64 -OutputDirectory ./artifacts/picket-linux-musl-x64 -ZstandardLibrary ./artifacts/zstd-musl/libzstd.so

The repository root Dockerfile builds a local Linux image from source using the same release-speed Native AOT profile as the release archives. Release jobs do not rebuild scanner code inside Docker. They extract the already tested linux-x64 and linux-arm64 release archives and build each image from packaging/container/Dockerfile on a native runner. The image includes:

  • picket as the entrypoint,
  • picket-tui beside it for picket tui,
  • git for Gitleaks-compatible picket git history scans,
  • CA certificates for source-host, object-store, and live-verification HTTPS calls,
  • Git system configuration that trusts /work for mounted repository scans,
  • a non-root default user from the .NET runtime-deps image.

Build locally for the current machine with:

Terminal window
docker build -t picket:dev .
docker run --rm -v ${PWD}:/work picket:dev scan . --report-format jsonl --redact=100

On Windows, run the WSL container CLI example from PowerShell:

Terminal window
wslc image build -t picket:dev .
wslc run --rm -v ${PWD}:/work picket:dev scan . --report-format jsonl --redact=100
wslc run --rm -v ${PWD}:/work picket:dev git . --report-format json --redact=100

For release tags, .github/workflows/release.yml validates and publishes linux/amd64 on an x64 runner and linux/arm64 on an Arm64 runner, then creates the multi-architecture manifest without emulation. The published tags are:

  • ghcr.io/willibrandon/picket:<release-tag>, for example v0.1.13,
  • ghcr.io/willibrandon/picket:<semver>, for example 0.1.13,
  • ghcr.io/willibrandon/picket:latest for non-prerelease versions only.

Each architecture image carries BuildKit SBOM and provenance attestations. The immutable version manifests are published only after the GitHub Release exists, and latest advances only after both the versioned container and NuGet publication succeed. The image is a scanner distribution surface; it does not change command defaults, compatibility behavior, reports, validation policy, or telemetry policy.

Release automation generates package-manager submission files from the actual release archive names and SHA-256 checksums:

  • homebrew/picket.rb for a Homebrew tap.
  • scoop/picket.json for a Scoop bucket.
  • winget/willibrandon.picket/<version>/ for WinGet package submission.

The generated files are packaged as picket-<tag>-package-manager-manifests.zip and uploaded to the GitHub Release with its own .sha256 sidecar. The final aggregate checksums.txt includes the manifest bundle.

Generate the same files locally from a release checksum file with:

Terminal window
dotnet run --file ./scripts/Generate-PackageManagerManifests.cs -- -ReleaseTag v0.1.13 -ChecksumsPath dist/checksums.txt -OutputDirectory artifacts/package-managers -Clean

The manifests install the Native AOT picket and picket-tui executables from the RID archives. Homebrew keeps the complete archive payload under libexec and writes command wrappers so the bundled zstandard library remains beside both executables. Scoop and WinGet retain the complete portable ZIP payload.

Windows portable archives contain only the supported picket.exe and picket-tui.exe entry points. CI and release jobs reject unexpected executables and invoke picket-tui without arguments under redirected input and output, matching WinGet’s post-install executable validation. In an interactive terminal, the same no-argument command opens the scan workspace.

For stable tags, the release workflow validates and commits Formula/picket.rb to willibrandon/homebrew-tap, commits bucket/picket.json to willibrandon/scoop-bucket, and updates both repository catalogs. It also validates the WinGet manifest set, pushes it to willibrandon/winget-pkgs, and opens or reuses a pull request against microsoft/winget-pkgs. Prerelease tags publish the manifest bundle with the GitHub Release but do not update package repositories.

Release automation builds MSI installers for stable win-x64 and win-arm64 releases from the same Windows release ZIP payloads. The WiX source lives at packaging/msi/Picket.wxs; it installs picket.exe, picket-tui.exe, libzstd.dll, LICENSE, and THIRD-PARTY-NOTICES.txt under Program Files\Picket and adds that install folder to the machine PATH.

The MSI artifacts are named:

  • picket-<tag>-win-x64.msi
  • picket-<tag>-win-arm64.msi

Each MSI is built, installed, and tested on a matching Windows runner architecture. The test installs into a temporary directory, exercises a zstandard scan through the installed executable, verifies the full payload, and uninstalls it before release assembly. Each artifact then receives a .sha256 sidecar and a GitHub artifact attestation. Prerelease tags skip MSI artifacts because Windows Installer ProductVersion does not support SemVer prerelease labels. MSI packaging does not rebuild scanner code and does not change CLI defaults, report behavior, validation policy, or telemetry policy.

CI installs the locked Azure DevOps packaging dependencies without lifecycle scripts, fails on any npm advisory, and packages the Marketplace scaffold on Ubuntu with tfx-cli and azure-devops/vss-extension.json. This validates the extension manifest, PicketScan@1 task metadata, included files, and VSIX layout before release automation consumes the same wrapper.

Release automation also packages stable-tag VSIX artifacts with the release version applied to the extension manifest:

  • picket-<tag>-azure-devops.vsix

The VSIX receives a .sha256 sidecar and a GitHub artifact attestation before the GitHub Release is created or updated. Prerelease tags skip VSIX artifacts because Azure DevOps extension manifest versions do not support SemVer prerelease labels.

For a stable tag, the Release workflow automatically revalidates the VSIX checksum, identity, required package contents, and artifact attestation before publishing it with AZURE_DEVOPS_MARKETPLACE_PAT. After Azure DevOps publication succeeds, the workflow advances the GitHub Action vMAJOR tag to the immutable release commit and verifies that GitHub Marketplace exposes the exact immutable release tag. GitHub requires the repository owner to publish the Action release through the release page before that verification can pass. Reruns detect an already-published Azure DevOps extension version and continue without attempting to overwrite it. See docs/MARKETPLACES.md for release ordering, visibility, and rollback behavior.

The docs workflow builds the static site, verifies generated docs are current, and checks local documentation links and fragments under the GitHub Pages /picket/ base path. External links are left to source review so the offline docs build stays deterministic.

CI runs the local composite action against the repository root on Windows, Linux, and macOS with cache, annotations, and SARIF upload disabled. The scan step keeps the Action summary enabled, uses fail-on: never for the repository’s intentional test fixtures, and asserts that the action reports at least one finding and writes both picket.sarif and picket.jsonl.

Use the target RID in the publish command. Common release RIDs are:

Platform RID
Windows x64 win-x64
Windows Arm64 win-arm64
Linux x64 linux-x64
Linux Arm64 linux-arm64
Linux musl x64 linux-musl-x64
Linux musl Arm64 linux-musl-arm64
macOS x64 osx-x64
macOS Arm64 osx-arm64

Release automation publishes, checksums, attests, and archives each RID separately. The picket and picket-tui binaries are staged into the same RID archive so picket tui can discover the companion beside the scanner.

Tags that match v*.*.* run .github/workflows/release.yml. The workflow can also be run manually for an existing tag.

The workflow validates the source tree and runs the local GitHub Action smoke test. It builds release-speed Native AOT binary archives for linux-x64, linux-arm64, linux-musl-x64, linux-musl-arm64, win-x64, win-arm64, osx-x64, and osx-arm64; builds and installs Windows MSI installers; packages the Azure DevOps VSIX; validates native container images; and packages the public NuGet libraries, top-level tool pointer packages, and RID-specific Native AOT tool packages.

The release assembly generates Homebrew, Scoop, and WinGet manifests from release checksums, writes per-asset .sha256 files, writes an aggregate checksums.txt, and creates or updates the GitHub Release for the tag. It also writes release-artifacts.json, a deterministic inventory of final payload names, exact byte counts, and SHA-256 digests. Checksum sidecars and checksums.txt are excluded from that payload-size inventory; the inventory itself is included in checksums.txt, attested separately, and published with the release. Only after that immutable release exists does the workflow publish .nupkg and .snupkg files to NuGet.org and per-architecture GHCR images. Stable releases then update the Homebrew tap and Scoop bucket, submit the WinGet manifest, publish the Azure DevOps extension, advance the GitHub Action major tag, and verify the Marketplace listing. The versioned container manifest follows its architecture images; the mutable latest tag waits for both NuGet and container publication.

GitHub’s supported Marketplace publication control is available only on the release page. For each stable Action release, edit https://github.com/willibrandon/picket/releases/edit/<tag>, select Publish this Action to the GitHub Marketplace, choose Security and Code quality, and publish the update with two-factor authentication. The release workflow’s final verifier fails until https://github.com/marketplace/actions/picket-secret-scanner reports that exact tag.

Release provenance uses GitHub artifact attestations through actions/attest@v4.2.0. GitHub’s current guidance for binary provenance requires id-token: write, contents: read, attestations: write, and a step that attests the built artifact. Consumers can verify a downloaded artifact with:

Terminal window
gh attestation verify <artifact-path> -R willibrandon/picket

Artifact attestations prove repository and workflow provenance but are not Authenticode signatures or Apple notarization. Windows executables and MSIs are currently unsigned, and macOS executables are currently unnotarized, so operating-system reputation checks can still warn on first use. Code signing and notarization require dedicated signing identities and remain a release-channel enhancement; the workflow does not suppress or bypass platform security checks.

The release tag is the source of truth for package versions. Release jobs strip a leading v from tags such as v0.1.13, pass the resulting SemVer value to Version and PackageVersion, publish package and symbol files with --skip-duplicate, and fail clearly when NUGET_API_KEY is missing.