Pakko — Developer Deployment Scripts

These scripts handle local MSIX signing and sideloading during development. They are not part of the build pipeline — run them manually from a PowerShell 7 (pwsh) terminal.


Prerequisites

  • Windows 10/11 with Developer Mode enabled, or sideloading allowed via Group Policy
  • .NET 10 SDK
  • PowerShell 7 (pwsh): the developer scripts have #Requires -Version 7.0, and the Release post-build deploy in Visual Studio runs pwsh.exe (T-F371). The two end-user scripts in "Permission repair for archives opened by older Pakko versions" below stay on Windows PowerShell 5.1.
  • Visual Studio 2026 with Desktop C++ and the MSVC v143 x64/ARM64 build tools (the scripts find MSBuild.exe via vswhere -latest; it builds only the C++ shell extension)

Step 1 — Set up the developer certificate (once)

.\scripts\Setup-DevCert.ps1

This will:

  1. Relaunch itself as Administrator if needed
  2. Create a self-signed CN=Pakko Dev code-signing certificate in Cert:\CurrentUser\My
  3. Export it as scripts/PakkoDev.cer (gitignored)
  4. Install it into Cert:\LocalMachine\TrustedPeople so Windows trusts signed packages

At the end it prints the certificate thumbprint — copy it for use with Deploy.ps1. You only need to run this once per machine (or when the certificate expires).


Step 2 — Build and install (after every change)

Full build + deploy (terminal workflow):

# Auto-detect the CN=Pakko Dev certificate (x64 default):
.\scripts\Deploy.ps1

# ARM64 build:
.\scripts\Deploy.ps1 -Architecture arm64

# Or pass the thumbprint explicitly:
.\scripts\Deploy.ps1 -Thumbprint "ABCDEF1234567890ABCDEF1234567890ABCDEF12"

This will:

  1. Publish Archiver.Shell, Archiver.OperationUi and Archiver.CLI as Native AOT exes (T-F355) for the target architecture, into the bin folders Archiver.App.csproj packages from. Needs the Visual Studio C++ build tools for that architecture; the script puts vswhere.exe on PATH, which ILCompiler uses to find them
  2. Build Archiver.ShellExtension.dll (MSBuild.exe directly on the .vcxproj, with /p:SolutionDir passed explicitly — see DECISIONS.md for why)
  3. Write src/Archiver.App/obj/PakkoDev/Package.appxmanifest, a copy of the tracked manifest whose revision is one past the installed dev package (X.Y.Z.0 with -SkipVersionBump), and pass it as /p:PakkoAppxManifest (T-F368: the tracked file stays at X.Y.Z.0, the Store rule)
  4. Run dotnet publish on Archiver.App.csproj with GenerateAppxPackageOnBuild=true and AppxPackageSigningEnabled=true + PackageCertificateThumbprint=<thumbprint> — packaging and signing happen in this one step. Content Include items in Archiver.App.csproj (conditioned on GenerateAppxPackageOnBuild=true) declare the three satellite exes and Archiver.ShellExtension.dll as package content (the App itself is Native AOT too, so the package has no .NET runtime files), so dotnet publish includes them automatically — there is no separate Archiver.Package.wapproj and no manual SignTool.exe call (a manual SignTool call on an MSIX produces ERROR_BAD_FORMAT; see DECISIONS.md "MSIX Signing")
  5. Uninstall any existing Pakko package
  6. Install the new .msix from src/Archiver.App/AppPackages/
  7. Print the installed version

-Architecture — "x64" (default) or "arm64". Derives the MSBuild Platform and runtime identifier automatically.

There is no Archiver.ProgressWindow project — it was removed (see DECISIONS.md, T-F65). Shell-triggered operations show progress via the Windows Shell's built-in IProgressDialog, in-process, no second .exe.

Deploy only (skips build — installs the most recently built .msix):

.\scripts\Deploy.ps1 -DeployOnly

Visual Studio post-build event — Release builds in Visual Studio run Deploy.ps1 -DeployOnly automatically after the build completes, so no manual script invocation is needed when building from VS.


Step 3 — Test the Explorer hand-off

After installing, run the installed Archiver.Shell.exe the way Explorer's "Extract files..." does (it opens Archiver.App through ActivateApplication; Pakko registers no URI scheme, T-F232):

$shell = (Get-AppxPackage PavloRybchenko.Pakko).InstallLocation + '\Archiver.Shell.exe'
Start-Process -FilePath $shell -ArgumentList @('--open-ui', '--extract', '"C:\path\to\file.zip"')

Pakko should open with the archive in its list. --browse instead of --extract opens it in the Archive Browser.


Publishing the standalone CLI (Archiver.CLI, T-F09)

Publish-Cli.ps1 is independent of everything above — the zip needs no dev-signing certificate. (Separately, since T-F317 Deploy.ps1 and CI-Build-Msix.ps1 also publish Archiver.CLI and package its pakko.exe into the MSIX for the pakko execution alias; CI-Build-Msix.ps1 -CliVersion X.Y.Z stamps a release version, and the script fails if the built package lacks one of the three satellite exes or the alias, or carries coreclr.dll or a satellite .dll - a sign a non-AOT build slipped in. It also copies the four exes' native .pdb files to artifacts/pdb/<arch>/, which CI keeps as the pakko-pdb-<arch> artifact for crash dumps.)

.\scripts\Publish-Cli.ps1                    # both architectures (default)
.\scripts\Publish-Cli.ps1 -Architecture x64  # one architecture only

Publishes a Native AOT build per architecture to artifacts/cli/<rid>/ (gitignored; T-F355, needs the C++ build tools of each architecture) — the built exe is pakko.exe (AssemblyName, distinct from the Archiver.CLI project/folder name) — zips pakko.exe alone as pakko-<rid>.zip (its pakko.pdb stays in the folder), and writes a SHA256SUMS file covering both zips — ready to attach directly to a GitHub Release. See CLI.md's "Distribution" section for why no tar.exe copy is bundled alongside it.

-OutputRoot <dir> publishes elsewhere. The folder is wiped first, so the script accepts only a new or empty folder, the default, or one it created before (it leaves a .pakko-cli-output marker); any other non-empty folder is refused (T-F259).

winget manifest for the CLI zip (T-F317)

New-WingetManifest.ps1 -Version X.Y.Z [-Sha256SumsPath <release SHA256SUMS>] [-ReleaseDate yyyy-MM-dd] writes the three manifest files for PavloRybchenko.PakkoCLI (moniker pakko-cli) into artifacts/winget/<version>/. A tag build's release job runs it too and uploads the result as the pakko-winget-manifest artifact. Per release, by hand (it publishes under the project's name):

  1. winget validate --manifest <folder>; optionally winget install --manifest <folder> (needs winget settings --enable LocalManifestFiles, admin) and pakko -v in a new terminal.
  2. Open a PR to microsoft/winget-pkgs adding the folder under manifests/p/PavloRybchenko/PakkoCLI/<version>/ (from the pakkoapp-oss account).

The manifest uses ArchiveBinariesDependOnPath: true — winget's Links symlink broke the pre-AOT apphost; a symlink is possible now (T-F361, see docs/CLI.md, Distribution).

PAR2 test oracles (T-F275)

.\scripts\Get-Par2Oracles.ps1                       # this machine's architecture
.\scripts\Get-Par2Oracles.ps1 -Architecture arm64

Downloads par2cmdline 1.4.0, par2cmdline-turbo 1.5.0 and MultiPar 1.3.3.6 (x64 only) into artifacts/par2-oracles/<tool>/, each checked against a pinned SHA-256 (the digest GitHub publishes for the release asset); a mismatch stops the script and leaves nothing behind. A tool already present at the pinned hash is not downloaded again. They are GPL-2.0, so they are never committed. What the tests use each for: docs/TESTING.md, "PAR2 Oracles".

Store listing text into a Partner Center export (T-F332)

py -3 scripts\Fill-StoreListing.py <export.csv> checks a listing CSV exported from Partner Center (submission overview, "Export listing") against docs/store-listing/<locale>.txt: every language column has a file and every file a column. With --write --output <filled.csv> it writes the description, short description, features, search terms, "What's new" and the copyright line from the files, copies the 300 x 300 icon's URL to every language, leaves every other row as exported, then reads the result back and compares it. --add-locale <tag> adds a language the export lacks (title and image rows of en-us); --keep-lists <tag> leaves one language's features and search terms as exported. The import itself ("Import listings") is done by hand.


Continuous Integration (T-F122)

.github/workflows/build.yml builds both artifacts automatically — it is a separate, CI-only path alongside everything above, not a replacement for local Deploy.ps1/Publish-Cli.ps1 use:

  • On every push to main and on pull requests into main: runs dotnet test --filter "Category!=Slow&Category!=VeryLarge" plus the C++ Archiver.ShellExtension.Tests suite. A red suite blocks every downstream job.
  • On every push to main (after tests pass): builds and signs the MSIX for both x64 and arm64 via a new CI-only script, scripts/CI-Build-Msix.ps1 (a Deploy.ps1 sibling covering just the build+sign steps — no install, no version bump), and publishes pakko.exe for both architectures via the existing Publish-Cli.ps1 unchanged. Both are uploaded as workflow artifacts.
  • On a version tag push (v*): additionally creates a real GitHub Release for that tag and attaches pakko-win-x64.zip, pakko-win-arm64.zip, and SHA256SUMS via the gh CLI. This is now the only planned CLI-Release publication path — there is no separate manual publish step to remember. The MSIX is not attached to the public Release (it's still signed with a sideload-only self-signed cert — see below); it stays a workflow-run artifact, downloaded manually to hand to testers, same as today.

Signing identity: CI signs with the exact same local CN=Pakko Dev dev cert Deploy.ps1 uses (thumbprint D2EC5F2C451ED0EBE94B8168A68E5B813954CC75), exported once as a PFX and stored as two repo secrets, PAKKO_DEV_CERT_PFX_BASE64 and PAKKO_DEV_CERT_PASSWORD. See build.yml's header comment for the exact swap point once T-F10 (SignPath Foundation) issues a real certificate — only those two secrets (and the thumbprint constant next to them) need to change, nothing else in the workflow.


Pinned SDK and lock files (T-F364)

global.json pins the exact SDK (rollForward: disable) and every project has a committed packages.lock.json; CI restores in locked mode (CI=true), so a changed package graph fails there instead of building. Native AOT puts the SDK's own ILCompiler/ILLink version into the locks, which is why the SDK is pinned as well (docs/DECISIONS.md, T-F364).

  • Changing a package: edit the .csproj, run dotnet restore, commit the changed packages.lock.json files with it.
  • A new SDK patch (the canary's canary-dotnet fails with "A newer SDK changes the lock files"): install that SDK, set global.json's version to it, run dotnet restore windows-archiver-wrapper.sln --force-evaluate, run the tests, commit global.json and the locks together.
  • A Visual Studio update removed the pinned SDK (every dotnet command says the SDK was not found): install exactly that version from the .NET download page, or do the bump above.
  • Dependabot's NuGet PRs: a week with none may mean its updater failed (e.g. it lacks the pinned SDK), not that nothing is outdated; read its job log under Insights, Dependency graph, Dependabot, nuget "/".

SBOM per artifact (T-F365)

New-Sbom.ps1 -Artifact Msix|Cli -Architecture x64|arm64 -Version <v> -OutputPath <file> writes the CycloneDX 1.6 SBOM of one shipped artifact. It restores that artifact's projects with --locked-mode, runs the CycloneDX tool pinned in .config/dotnet-tools.json, removes what the artifact does not ship (build tools, Windows ML, other architectures' runtime) and checks the result (docs/DECISIONS.md, T-F365). CI runs it in the sbom job, which has no secrets; the build jobs attest each SBOM against its artifact, and a release carries pakko-msix-<arch>.cdx.json and pakko-win-<arch>.cdx.json.

  • Check a downloaded artifact's SBOM attestation: gh attestation verify pakko-win-x64.zip -R pakkoapp-oss/pakko --predicate-type https://cyclonedx.org/bom (the same without --predicate-type checks the SLSA provenance).
  • Bumping the tool: Dependabot is not known to update .config/dotnet-tools.json; change the cyclonedx version there by hand, run the script for all four artifacts and diff the component lists before committing.

Canary build (T-F187)

A separate workflow, .github/workflows/canary.yml, runs daily (cron: "17 6 * * *") plus workflow_dispatch on demand. Unlike build.yml above, it runs on a deliberately floating toolchain (windows-latest, a floating .NET 10 SDK, T-F364) instead of the pinned windows-2022/MSVC v143 combination build-msix relies on for stability. The goal is to catch SDK/NuGet/MSVC toolset drift on the day it actually happens, rather than waiting for someone to eventually bump build.yml's pin and discover the break then (the exact class of surprise T-F122 already produced once).

canary-dotnet runs the same dotnet test --filter "Category!=Slow&Category!=VeryLarge" command as build.yml's own test job; canary-shellext compiles Archiver.ShellExtension.vcxproj directly, x64 only — ARM64-on-windows-latest is already a known, unrelated, unfixed failure (MSB8020, see build.yml's own header comment / T-F122), so it's deliberately excluded here to avoid escalating a permanent condition as if it were new drift. No signing, no MSIX packaging — that stays covered entirely by build.yml's own pinned path.

Escalation, not immediate alerting: every step in both build jobs is wrapped in continue-on-error: true, so a transient network/runner blip is retried in-run (2 attempts) and never reddens the workflow by itself. A genuine failure only shows as a ::warning:: log annotation for its first two occurrences — no red X, no email. Only the 3rd consecutive scheduled-day failure fails the run for real (GitHub's default failure email) and creates/updates one de-duplicated tracking Issue titled CI Canary: build failing for 3+ consecutive days, which auto-closes on the next green run. Once escalated, the run keeps failing daily (Issue updated via comment) until the underlying build is actually fixed — it doesn't go quiet just because the Issue already exists.

The 3-day streak is reconstructed from a dedicated sentinel job, canary-failed-day (runs only when a day fails; its own recorded conclusion across past runs, queried via the Actions REST API, is the one bit of state this workflow persists) — not from the workflow run's own conclusion, which is deliberately masked on days 1-2 and so cannot double as the failure signal. canary-status computes today's result and the streak but always exits 0 itself; canary-alert is the only job that can actually fail or touch the tracking Issue — check its log first when investigating an escalation. See docs/DECISIONS.md's T-F187 entry for why the more obvious single-job approach doesn't work.

Note: GitHub auto-disables scheduled workflows after 60 days of repository inactivity — if the canary goes silent, check whether it's actually still enabled (Actions tab) before reading silence as "everything's fine."


Store-submission builds (build-store-msix, T-F129)

A separate, workflow_dispatch-only job in the same build.yml, for packages actually uploaded to Partner Center — kept out of the automatic push/tag pipeline since a real Store submission is a deliberate, occasional act, not something that should fire on every commit.

Why a separate job at all: dotnet publish's MSIX packaging rewrites the built package's Identity/Publisher to match whatever certificate signs it. Every package built with the tester-facing CN=Pakko Dev cert above therefore ships with Publisher="CN=Pakko Dev" — not Pakko's reserved Partner Center identity (CN=EF3EC84C-8287-4FC3-BB4F-FCCEBA116BCE) — and Partner Center rejects it outright. Confirmed against a real submission attempt, 2026-08-01: both Invalid package publisher name and the derived Invalid package family name errors clear once signed with the correct-Subject cert. See docs/DECISIONS.md's T-F129 entry for the full trail.

Run it: Actions tab → Build → Run workflow, against the tag/ref you're about to submit. Two architecture legs (x64, arm64) build+sign on windows-2022 (build-store-msix, same ARM64-v143-toolset reasoning as build-msix) as single-architecture .msixbundles, then a third job (bundle-store-msix) unbundles both, merges them into one real multi-architecture .msixbundle, and re-signs it — download that single pakko-store-msixbundle artifact from the run's Summary page and upload it to Partner Center's Packages step. Not attached to the public GitHub Release: a differently-signed package with a different Publisher would be confusing for end users to stumble onto, and Microsoft re-signs for real distribution anyway once certification passes, so the local signature here only needs to make packaging/upload succeed.

Why the merge step exists: a .msixbundle's own Identity carries no ProcessorArchitecture attribute — only the packages inside it do. Two separately-built single-architecture bundles (one containing only the x64 .msix, one containing only the arm64 .msix) therefore compute to the exact same full package name (..._Neutral_...) despite having different contents byte-for- byte, and Partner Center rejects the upload outright: "All .msix and .appx packages ... must be uniquely identified by their full names." Confirmed against a real submission, 2026-08-01. The fix is makeappx unbundle on each, makeappx bundle /bv <version> on the combined inner .msix files, then signtool sign again (bundling strips the original per-bundle signature) — this is also Microsoft's own documented shape for a multi-architecture Store app, not a workaround.

Local machines are not the reference for this build. scripts/Setup-StoreCert.ps1 exists so a developer can build and smoke-test a Store-identity package locally (useful for verifying the fix before relying on CI, or if the cert ever needs rotating), but the artifact actually uploaded to Partner Center should always come from this CI job, not a local Deploy.ps1 run — confirmed 2026-08-01 that a real dev machine may simply be missing the ARM64 v143 C++ toolset MSVC needs (a genuine, not-uncommon local environment gap; windows-2022 runners have it out of the box).

Signing identity: a second self-signed cert, Subject CN=EF3EC84C-8287-4FC3-BB4F-FCCEBA116BCE (thumbprint CD8DE1646CBF5A52046001FB32B0B60B797E7497), created via Setup-StoreCert.ps1 and exported once as a PFX into two more repo secrets, PAKKO_STORE_CERT_PFX_BASE64 and PAKKO_STORE_CERT_PASSWORD — same pattern as the dev cert pair, just a different Subject/secret names. This is also a self-signed cert, not a "real" purchased one: Partner Center accepts self-signed-cert-signed uploads routinely, since Microsoft re-signs on successful certification — the Subject matching the reserved Publisher is what actually matters here, not the cert's issuer.


SonarCloud static analysis (T-F135)

The test job runs a SonarCloud scan wrapped around the existing dotnet test invocation (JDK setup → dotnet-sonarscanner begin → dotnet test → dotnet-sonarscanner end), on every push to main/tags and on same-repo pull requests. Free tier — SonarCloud analysis is free for genuinely public repositories, which this one is. Fork-originated PRs skip the Sonar steps entirely (no SONAR_TOKEN secret available to them) rather than failing.

Real organization/project keys (confirmed via SonarCloud's public API, 2026-07-27) — note the -1 suffix on the org key, since the plain pakkoapp-oss key was already taken on sonarcloud.io when the project was created, so it does NOT match the GitHub org/account name:

  • Organization key: pakkoapp-oss-1
  • Project key: pakkoapp-oss-1_pakko

Both are wired into build.yml's SONAR_PROJECT_KEY/SONAR_ORGANIZATION env vars and into README.md's badge — don't reuse pakkoapp-oss/pakkoapp-oss_pakko (an earlier guessed placeholder that turned out wrong) anywhere new.

Coverage: the org's default "Sonar way" quality gate requires new code coverage ≥ 80% — with no coverage data submitted, that condition reads as "no data" and fails the gate outright. The test job's dotnet test step therefore also passes --collect:"XPlat Code Coverage" (using the coverlet.collector package every test project already references — no new dependency), and the dotnet-sonarscanner begin step passes /d:sonar.cs.cobertura.reportsPaths="**/coverage.cobertura.xml" to pick up the resulting per-test-project Cobertura reports. This makes the condition evaluate against a real number — it does not guarantee the actual number clears 80%; if the real coverage on new code comes in lower, the gate can still fail, and that's a legitimate signal to act on (write more tests), not a CI bug.

One-time setup (already done 2026-07-27 — kept here for reference / re-setup):

  1. Sign in to sonarcloud.io, create the org, import the pakkoapp-oss/pakko repository as a new project (done manually, without GitHub-App binding, since the importing account had no Admin on the repo — see DECISIONS.md's T-F135 entry).
  2. Generate an analysis token (My Account → Security) and add it as a repo secret named SONAR_TOKEN (Settings → Secrets and variables → Actions) — done.
  3. Analysis Method should already be "CI-based" — the manual/"Other CI" dotnet-sonarscanner setup snippet SonarCloud generated (see below) only exists for CI-based projects; Automatic Analysis needs the GitHub App installed with repo Admin, which was never available for this import (step 1). Not independently eyeballed on the Administration → Analysis Method page, but this project structurally couldn't have ended up in Automatic mode.
  4. Once a real scan has run, confirm the quality-gate results against the actual SonarCloud project dashboard (https://sonarcloud.io/project/overview?id=pakkoapp-oss-1_pakko), not just a green GitHub Actions run — same rule this project already applies to every other CI change (T-F122/T-F125 precedent).

The C++ Archiver.ShellExtension/Archiver.ShellExtension.Tests projects are not covered by this scan (SonarCloud's C/C++ analysis needs a separate build-wrapper tool and has its own licensing terms — not yet evaluated for the free tier). Only the .NET projects are analyzed for now.


Measuring start-up (T-F346)

.\scripts\Measure-Startup.ps1                          # all scenarios, 8 launches each
.\scripts\Measure-Startup.ps1 -Runs 12 -Scenario App,Open

Times the installed package (run Deploy.ps1 first): App start to visible window with its memory, threads and CPU; Explorer's "Open" (Archiver.Shell.exe --open-ui --browse) to visible App window; Explorer's "Extract here" on a generated 200-file ZIP (files written, Shell exit); pakko --help and pakko l; the operation window on a generated 4000-file ZIP and on a conflict prompt (Shell start to visible Archiver.OperationUi window). Prints median, minimum and maximum; the first launch of each scenario is dropped. There is no pass/fail threshold — the numbers depend on the machine, so compare a run before a change with a run after it on the same machine. It opens and closes Pakko windows on the desktop while it runs.


Agent hook (T-F370)

.claude/settings.json (tracked) runs hooks/Test-AgentBashCommand.ps1 before every Bash-tool call Claude Code makes in this repo. It blocks bare python/python3 (use py -3 script.py) and dotnet with a /p: switch (Git Bash rewrites /p: into a path; use the PowerShell tool), at command position only, so quoted text and heredoc bodies pass. Exit 2 blocks; a payload that is not JSON exits 1 and does not block. Tests: AgentBashHookTests.

Notes

  • PakkoDev.cer and PakkoStore.cer are gitignored — never commit certificates to the repository.
  • All paths in the scripts are resolved relative to $PSScriptRoot, so they work regardless of your current working directory.
  • The self-signed certificate is for local development only. Store/release builds require a trusted EV certificate (see T-F10).

Permission repair for archives opened by older Pakko versions (T-F233)

Pakko versions before fix phase 4 (2026-09-25) changed the permissions of tar-family archives (.tar, .gz, .7z, .rar, ...) they opened: an entry for the sandbox was added, and the entries the file inherited from its folder were replaced. Two scripts, for users who ask (Windows PowerShell 5.1 is enough):

.\scripts\Find-PakkoSandboxAce.ps1                    # read-only: lists affected files
.\scripts\Find-PakkoSandboxAce.ps1 -Path D:\Shared -AllFiles
.\scripts\Repair-PakkoSandboxAce.ps1 -Path '<file>' -WhatIf
.\scripts\Repair-PakkoSandboxAce.ps1 -Path '<file>'   # backs up with icacls /save first

The repair removes only Pakko's own entries and re-inherits from the file's folder; entries set for anyone else are kept (it is not icacls /reset). See docs/DECISIONS.md's fix-phase-4 entry.

Invoke-Smoke.ps1 — the on-device checks (T-F378)

Runs rows of docs/SMOKE.md against the installed package. The catalogue says what each row does and expects; this script is a filter over it.

.\scripts\Invoke-Smoke.ps1                       # the quick tier: 25 rows, about 2.5 minutes
.\scripts\Invoke-Smoke.ps1 -Tier release         # quick and release rows: about 26 minutes
.\scripts\Invoke-Smoke.ps1 -Tier release -Only CLI-0*   # the console rows: about a minute
.\scripts\Invoke-Smoke.ps1 -Tier release -Only PAR-*    # the PAR2 rows: about a minute
.\scripts\Invoke-Smoke.ps1 -Only PAR-*,CLI-004   # some rows of the chosen tier
.\scripts\Invoke-Smoke.ps1 -Tier store -List     # show the rows, run nothing
# Before a tag: the release tier with the update rows (<run> = the build run of the release commit)
gh run download <run> -n pakko-msix-x64 -D $env:TEMP\new; gh release download <last tag> -D $env:TEMP\last
.\scripts\Invoke-Smoke.ps1 -Tier release -PackageLifecycle -ReleaseFiles $env:TEMP\new -PreviousRelease $env:TEMP\last
# After it: everything, on the release's own files
gh release download <tag> -D $env:TEMP\new
.\scripts\Invoke-Smoke.ps1 -Tier store -PackageLifecycle -Tag <tag> -ReleaseFiles $env:TEMP\new -PreviousRelease $env:TEMP\last
  • Install the build first with Deploy.ps1; the script tests what is installed. -PakkoExe <path> tests another pakko.exe (the one from a release zip) in the CLI, PAR2 and speed rows.
  • A row a script can run has a step in scripts/smoke/<Surface>.ps1, registered with Add-SmokeStep '<ID>' { ... }; Common.ps1 holds the helpers, and tools/ the scripts a step starts as a process of their own (Send-CtrlC.ps1: Ctrl+C goes to a console, so the sender joins the target's). A step throws to fail (Assert-Smoke), calls Skip-Smoke '<reason>' when it cannot run here, and may return a note. Rows without a step are printed as manual with their action and expectation.
  • Fixtures go into a new folder under %TEMP% and are removed at the end; the folder is kept when a row fails. The result table is printed and written to %TEMP%\pakko-smoke-report.md; paste it into the release record.
  • Windows open and close on the desktop during a run, and the speed rows compare times: leave the machine alone meanwhile. The PAR2 rows need Get-Par2Oracles.ps1 to have been run once, and SHL-008 writes a ZIP with the antivirus test string into the run's folder: the antivirus may report it. REL-001 is dotnet test --filter "Category=Slow" on the checked-out commit and takes about twenty minutes; REL-002 reads the canary run on that commit and is skipped when there is none.
  • The POL rows and PKG-005 need an administrator: the first of them raises one elevation prompt for scripts/smoke/elevated/PolicyHost.ps1, which sets Pakko's machine policies and the Windows FIPS flag for one row at a time, runs appcert.exe, and puts the registry back. Decline the prompt and those rows are skipped. -Hold keeps each row's policy set until a file named go is created in the run's folder: the time to look at the App and the Explorer menu under it (POL-009, POL-010).
  • PKG-006..009 remove and replace the installed package, so they are skipped without -PackageLifecycle. With it they run last, and the run ends with the package it began with (its file must still be under src/Archiver.App/AppPackages); the package's data is lost, as on every Deploy.ps1. PKG-006..008 update from one bundle to another and also need -PreviousRelease <folder> (gh release download <last tag> -D <folder>) and -ReleaseFiles <folder>: before the tag, the bundle CI built on the release commit (gh run download <run> -n pakko-msix-x64 -D <folder>), after it the release's own files. With -Hold, PKG-009 waits with the package removed (PKG-010).
  • The store rows (CLI-017, REL-003..007, STO-001..003) check the release's files against what -Tag stands for: its version, its commit, its CHANGELOG.md. A whole -Tier store run refuses to start without -Tag, -ReleaseFiles, -PreviousRelease and -PackageLifecycle. They need gh (signed in) and the network; REL-007 installs the CLI with winget and removes it, and is skipped when the CLI is already installed that way. STO-001 and STO-002 read the Store bundle of a dispatch run on the tag and never start one.
  • Shell and App windows speak the user's language, so steps match numbers, names and hashes in their text, never words.
  • Adding a check: add the row to docs/SMOKE.md (the next free ID of its surface), then its step. SmokeCatalogueTests fails when a row marked script or uac has no step or a step has no row.
  • A step that starts pakko gives it a time limit (Invoke-Tool -TimeoutSeconds, 15 minutes unless set): a hang fails the row instead of the run.