Repository navigation
Release pipeline: build into the draft and publish only when every file is there; pinned winget tooling; signed installs - #557
Merged
Merged
Conversation
actions/checkout persists GITHUB_TOKEN into .git/config by default, so every later step in the job can read it, including cargo build scripts, npm install scripts and npx tools in jobs that hold contents: write. Nothing in these workflows pushes through the checkout: the tap and bucket scripts clone with their own token, and the two git fetches in release.yml read a public repository. So every checkout now sets persist-credentials: false. Audit M19 (ci-release M-3).
dtolnay/rust-toolchain@stable was the last action reference on a moving branch. Pinned to the same commit every other job uses. Audit ci-release L-1.
Nine of the ignores named advisories that cannot fire against the current Cargo.lock, and an ignore that matches nothing would still hide the advisory if the crate came back: - paste (RUSTSEC-2024-0436), fxhash (RUSTSEC-2025-0057) and the five unic-* crates (RUSTSEC-2025-0075/0080/0081/0098/0100) are no longer in the lockfile. - RUSTSEC-2026-0097 was justified as rand 0.7.3 via kuchikiki. Neither is in the lockfile, and the rand versions that are (0.8.6, 0.10.1) are the patched ones. - RUSTSEC-2026-0002 (lru) is patched from 0.16.3, and the lockfile has lru 0.18.2. The proc-macro-error rationale named dialoguer, which does not pull it. glib-macros and gtk3-macros do. cargo audit --no-fetch (cargo-audit 0.22.2, local advisory db) reports the same as before the change: no vulnerabilities, 3 allowed warnings. Audit ci-release L-13.
Two manifests had no update entry: - scripts/mcp-smoke (npm), the SDK-client smoke test for netscli serve. Keeping its SDK current is the point of the test. - scripts/linux-check.Dockerfile (docker). Its rust base image tracks rust-toolchain.toml, so a PR there is the one automatic signal that a new toolchain is out. The comment says what else has to move with it. packaging/npm/netscli/package.json stays out on purpose and now says why: it is a template full of @@Version@@ placeholders whose dependencies are this project's own packages. Audit ci-release I-3.
WINGET_TOKEN was handed to code nobody pinned. The SHA-pinned vedantmgoyal9/winget-releaser installs cargo-bins/cargo-binstall@main, uses it to install whichever komac is newest, and gives that binary the token, a classic PAT that can write to every repository its owner can. Both winget jobs now do what the action did, in three steps of their own: download komac 2.16.0's Windows binary and refuse it unless its SHA-256 matches the value pinned at the top of the workflow (taken from Komac's SHA256SUMS on the v2.16.0 release, which the GitHub API's asset digest agrees with), run komac update --submit for the same identifier and installer URL as before, then komac cleanup --only-merged. The token is in the environment of the last two steps only, never on a command line. Komac 2.16.0 derives the fork from the token's owner, so moving the token to a bot account needs no workflow change. Audit H5 (ci-release H-1).
A release went public first and was built afterwards. v0.3.4 had no
Windows installer and no latest.json for 1 h 38 min after it was
published, so install.ps1, the in-app updater and the package managers
that had already published pointed at files that did not exist. v0.3.3's
Windows files were 1 h 06 min late.
publish-release.yml now runs the whole sequence as three jobs:
- prepare: tag well formed and pushed, run from main, release still a
draft, CHANGELOG.md has the version's section.
- build: calls release.yml (now also a reusable workflow) against the
still-draft release.
- promote: scripts/release/check-release-assets.mjs requires all 18
files with their .sha256/.sig/.pem, every .sha256 equal to the digest
GitHub computed for the upload, and a latest.json for this version
pointing at this tag. Only then are the notes written, the draft made
public and read back, and publish.yml and pages.yml dispatched.
The lock against release-drafter is unchanged and is held for the whole
run.
release.yml:
- Drops the `release: published` trigger, so promoting the draft cannot
start a second build that would replace published assets with new
digests. A new guard job refuses a malformed tag, a run off main, and
a release that is already public, so a manual dispatch cannot clobber
a published release either.
- Uploads with `gh release upload --clobber` instead of
softprops/action-gh-release, which PATCHes the release on every upload
and, with `draft` unset, publishes the draft once its files are up.
- sign-windows runs when a Linux or macOS leg failed (!cancelled()), so
one attempt surfaces Windows signing problems too. It still requires
all three Windows files. updater-manifest gets an explicit condition
so a failed CLI leg does not skip it through the needs chain.
Re-runs: a failed leg is re-run from the publish-release run ("Re-run
failed jobs"), which re-runs everything after it, promote included. A
re-run that finds the release already public stops with the commands to
finish by hand, so the fan-out never runs twice.
The new check has tests (node --test, wired into CI's script-lint job).
Against the real v0.3.4 asset list it passes. With v0.3.4's Windows
files and latest.json removed, as they were for 1 h 38 min, it fails
naming all 13 missing files. A sabotaged copy that accepts everything
fails 10 of the 13 tests.
Audit H6 (ci-release H-2), ci-release L-2.
The Windows MSI and Linux AppImage signatures in 0.3.4's latest.json carry no signed version (decoded from the published file: macOS entries end in version:0.3.4, the other two have none). Both files are re-signed after the build, and the re-sign did not pass a version. With requireSignedVersion off, the plugin accepts such a signature, so anyone able to edit a release's latest.json could pair a higher version number with an older, validly signed installer. - release.yml re-signs the AppImage and the MSI with `tauri signer sign --app-version`. macOS keeps the bundler's signature, which already carries the version. - sign-windows installs the Tauri CLI in a step of its own, with no secret in it and --ignore-scripts, at the version package-lock.json locks. It used npx inside the signing step, beside the key, at a hand-typed 2.11.5 that had drifted from the lockfile's 2.12.1. - updater-manifest.mjs refuses a signature not bound to the release's version, and check-release-assets.mjs checks the uploaded latest.json the same way, so a missing --app-version fails the draft, not users. - tauri.conf.json sets plugins.updater.requireSignedVersion. Supported by the locked tauri-plugin-updater 2.13.1 (config.rs) and 2.12.0. Checked with the locked CLI 2.12.1 and a throwaway key: a signature made with --app-version 0.3.5 carries version:0.3.5 and builds latest.json, one made without it is refused. A copy of updater-manifest.mjs without the new check fails 2 of the 5 new tests. Audit desktop-app F7, ci-release L-6 (Tauri CLI pin).
…steps The GUI job's single `tauri build` step held TAURI_SIGNING_PRIVATE_KEY and its password on every leg, and CERTUM_EMAIL/CERTUM_OTP on Windows, while it compiled the whole Rust dependency graph (build scripts and proc macros) and ran beforeBuildCommand (tsc and Vite). `npm ci`, with install scripts enabled, ran earlier in the same job. Both keys are long-lived, and the updater key cannot be replaced without every user reinstalling. - `tauri build` is now `tauri build --no-bundle` with no secrets, then `tauri bundle` with them. That is what `tauri build` does internally (tauri-cli 2.12.1 build.rs calls the same bundle function), with the same --config files on both steps. - `npm ci --ignore-scripts`. The lockfile's only install scripts are edgedriver (end-to-end suite) and fsevents (optional), which the build does not need. The bundle step still holds the keys, because the bundler signs the update files and runs signCommand. A step boundary is not a sandbox, the workflow comment says so. Audit M18 (ci-release M-2).
… bundles crates.io (M21, L-4) - The token was on the `cargo publish` command line, in the same step that builds every packaged crate, so every build script ran with it in its environment and argv. Now `cargo publish --dry-run` packages and builds with no token, and `cargo publish --no-verify` uploads with the token in CARGO_REGISTRY_TOKEN and compiles nothing. - A re-run after a partial upload failed on the crate that was already up and never reached the rest. The job now asks crates.io which of the three already have the version and publishes only the others. MCP bundles (M23, M19) - The five .mcpb files were the only executable-carrying assets with no .sha256, .sig or .pem. A new mcpb-sign job hashes and cosign-signs them the way release.yml signs everything else (identity: publish.yml@refs/heads/main) and uploads all three files per bundle. - The bundles are built with `npx @anthropic-ai/mcpb`, whose dependencies are caret ranges resolved on every run. That job now has a read-only token and no OIDC. The new job, which holds both, runs no package code. Tooling and pins (L-5, L-6) - homebrew, scoop, aur and their GUI siblings check out the default branch, as npm, mcpb and mcp-registry already did, so a fix to their scripts reaches a re-run. crates-io keeps the tag: it builds the source. - npm for trusted publishing is pinned to 11.21.0 instead of ^11.5.1. The summary points at `-f only=<job>` for re-running one registry, and the `only` comment lists the job ids. Audit M21, M23, ci-release L-4, L-5, L-6.
netscli.com showed the unpublished 0.3.5 as current: the JSON-LD softwareVersion came from apps/netscli-gui/package.json, and the no-script changelog page dated 0.3.5 "Oct 6, 2026" from its CHANGELOG heading. Pages deploys from main, where a version is bumped, tagged and dated before its release is published, so nothing in the repository can say which versions are out. GitHub can. - version.ts reads NETSCLI_PUBLISHED_RELEASES, which pages.yml will set from the Releases API. The site's version is the newest published release. Set but empty (GitHub could not be asked) means no version, and Page.astro then leaves softwareVersion out of the JSON-LD rather than guess. Unset (local, CI and preview builds) falls back to package.json as before. - changelog.astro's build-time render gives every entry GitHub does not list "Not yet released" and no date, as the client already does once GitHub answers. With an empty list, no entry is dated. - check:changelog checks that rule when run with the variable set, and keeps the old check without it. - CHANGELOG.md's preamble said a date goes on "only once its release is published" and, three lines later, "with the tag". It now gives the one rule the process follows: the date goes on once the tag is pushed, is the intended publish day, and says nothing about publication. Built locally three ways. Unset: unchanged (softwareVersion 0.3.5, v0.3.5 dated), check:changelog passes. With the 13 tags GitHub lists today: softwareVersion 0.3.4, v0.3.5 "Not yet released" and undated, v0.3.4 and v0.3.3 dated, check passes. Empty: no softwareVersion, no dates, check passes. The check, run with today's list against the page as it is deployed now, fails on 0.3.5. astro check: 0 errors, 0 warnings; check:structured-data passes on all three builds. Audit M26 (site-docs-copy F6, repo-docs 8).
pages.yml (M22, M26, L-7) - Three jobs. `build` runs npm ci and astro build with a read-only token. `stage` runs no npm: it requires site/dist/install.sh and install.ps1, which the one-liners pipe into a shell, to equal this commit's scripts/install.* byte for byte, refuses a download/ directory it did not write, stages the Store MSIs and checks each one's Authenticode signature with osslsigncode (signer CN "Open Source Developer Felix Stubner", as printed in the v0.3.3 sign-windows log). `deploy` is the only job with pages: write and id-token: write. Installers from before v0.3.3 were never signed and are staged as published; v0.3.2 is among the three staged today. - Asks the Releases API which releases are published and passes the list to the build in NETSCLI_PUBLISHED_RELEASES, then runs check:changelog on the build that ships. - timeout-minutes on all three jobs. stage-store-downloads.sh (L-7) - Lists each release's assets instead of reading any failed download, with its error output discarded, as "no MSI". An API or network error used to skip the newest release while older ones staged and the job passed. Audit M22, M26, ci-release L-7.
publish-preflight.yml (M24, M35) - A new `signing` job, in the release-signing environment, checks the four secrets nothing checked before a release. The updater key and its password sign a scratch file with the CLI version the app locks, and minisign verifies it against the public key in tauri.conf.json, so a wrong password or a key the app does not trust fails here instead of a quarter of an hour into the build. CERTUM_EMAIL and CERTUM_OTP are checked for presence and shape only, since a real check would log in to Certum. - WINGET_TOKEN is now also checked for the `workflow` scope (from the X-OAuth-Scopes header), which Komac needs to create its branch and which a `public_repo`-only token lacks while still authenticating. The result names the token's owner, so a later move to a bot account shows. ci.yml (M24) - PSScriptAnalyzer runs over scripts/release/*.ps1 as well as install.ps1, with the same exclusions. Not run locally: pwsh is not available in this environment, so CI is the first run. Audit M24, M35 (ci-release M-8, repo-docs 5).
Structure only. Piped from curl, bash runs the script as it arrives, so a download cut short ran whatever part had arrived. The banner, the platform detection and the install itself now sit in main(), which is called on the script's last line, so a truncated download runs nothing. Every moved line is unchanged apart from its indentation (git show -w). Audit ci-release L-9 (part one).
…only M20: the installer checked only a SHA-256 fetched from the same release as the binary, which proves the download is intact but not who built it. With cosign installed it now also verifies the binary's .sig/.pem, and requires the signer to be release.yml in this repository on main or a release tag (not the looser `release\.yml@.*`, which accepts a signature made by that file on any branch). A signature that is missing or does not verify stops the install. Without cosign it installs as before. Either way it prints what it checked and what it did not. NETSCLI_SKIP_SIGNATURE=1 is the documented way past the check. L-9: curl now runs with --proto '=https' --proto-redir '=https' --tlsv1.2. Every GitHub release download is a redirect, and curl follows redirects to plain HTTP by default. install_test.sh runs it against a fake release (fake curl, uname and cosign, no network), and CI runs it in script-lint. All 11 cases pass; run against the original install.sh, 8 of them fail. Audit M20 (ci-release M-4), ci-release L-9, L-11.
Every failure path called `exit 1`. The documented invocation is `iwr ... | iex`, which runs the script inside the caller's own PowerShell session, so `exit` ended that session and closed the window with the error message in it. The top-level variables and $ErrorActionPreference = 'Stop' were also left behind in the session. The procedural part now runs in Install-NetsCLI, called on the last line, and each `exit 1` is a `throw` naming what went wrong, after the same red messages as before. A thrown error leaves an interactive window open, and still ends `powershell -File` or `-Command` with exit code 1. Every other moved line is unchanged apart from its indentation (git show -w). Not run locally: pwsh is not available in this environment, so CI's PSScriptAnalyzer step is the first check. Audit ci-release L-8.
The installer checked only a SHA-256 fetched from the same release as the binary, which proves the download is intact but not who built it, although every netscli.exe since v0.3.3 is Authenticode-signed. It already checked the Npcap installer's signature, but not its own binary. After the checksum it now requires Get-AuthenticodeSignature to report Valid and the signer CN to be "Open Source Developer Felix Stubner" (the subject the v0.3.4 release log shows for the project's Certum certificate). A bad signature stops the install. It says so, and checks nothing, for an explicitly pinned NETSCLI_VERSION older than v0.3.3, for a REPO other than fstubner/netscli unless NETSCLI_SIGNER names its signer, and where Get-AuthenticodeSignature is missing. NETSCLI_SIGNER and NETSCLI_SKIP_SIGNATURE=1 are the overrides. Before installing it prints what it checked. Not run locally: pwsh is not available in this environment, so CI's PSScriptAnalyzer step is the first check of this file. Audit M20 (ci-release M-4).
The Entra client secret was a --data-urlencode argument and the bearer token an -H argument, so both sat in curl's argv, readable by any process on the runner. The secret now reaches curl on stdin (client_secret@-), and the token is written to a header file in a private temporary directory and passed as -H @file. The tag is checked with lib.sh's validate_tag before it reaches the package URL. msstore-submit_test.sh needs jq, which this machine lacks, so it was not run here. It runs in CI's script-lint job. Audit ci-release L-14.
- audit.yml: cargo-audit's cache key was the constant `cargo-audit-v1`, which the weekly run kept warm, so it was never upgraded. It is now keyed on CARGO_AUDIT_VERSION (0.22.2), which the install also pins. - timeout-minutes on cargo audit, every site.yml job and the preview job, which ran with GitHub's 360-minute default. Sized from measured runs: Site's build took 3.5 min (run 37385259611), the preview 43 s (run 37398996660). - site-preview.yml: `npx wrangler@4` installed the newest 4.x on every run and handed it CLOUDFLARE_API_TOKEN. Pinned to 4.148.0. Its header and PR comment still said pages.yml is manual-only; it deploys on pushes to main. Audit ci-release L-6, L-7, repo-docs 20(a).
Four comments gave build and job durations nobody measured ("40
minutes", "a quarter of an hour", "in a minute", "a few minutes").
They now say what happens without a number.
RELEASE.md and PUBLISHING.md were one pipeline generation behind (audit M32-M37) and now describe this branch's pipeline. RELEASE.md, the process: - The flow is bump, tag and push, date the changelog and write the release summary, preflight, then publish-release.yml, which builds into the draft, checks the assets and only then publishes. It no longer tells you to create a second draft or hand-edit notes the workflow overwrites, and it says to push the tag (M33). - "When something fails" gives the real re-run paths, including `-f only=<job>` with every job id (M32), the one-day limit on the unsigned Windows files, and the lock's queueing caveat (L-3). - The release-signing environment section asks for the main + v* policy instead of forbidding one. The winget token section explains the workflow scope and the bot-account move (M35). - A staged in-app update check, against a local server and a throwaway key, as a release step (M16). - Windows signing, the Store and the updater are described as shipped, not deferred (M36). The cosign identity is the exact one (L-11). The NSIS smoke, the yay line and the "tag refs" claim are gone. PUBLISHING.md, the per-channel reference: - All channels and all 13 publish jobs, the 73-file asset set, npm, the MCP Registry, MCP bundles and netscli.com (M34). - crates.io is the automated job, with the hand-run command as a fallback after publication. The manual three-step flow and `sleep 90` are gone (M37). - winget via the pinned Komac, Homebrew regenerated from its template, site previews as they behave, the dating rule as the CHANGELOG preamble now states it, and a bump recipe that covers package-lock.json. publish.yml's mcpb-sign comment names both identities it can sign as.
Changed: releases are published only once complete; MCP bundles carry checksums and signatures. Fixed: the PowerShell installer no longer closes the window when it fails. Security: both install scripts check signatures; the desktop app requires update signatures bound to their version.
The comments, the 0.3.5 changelog entry and the H6 commit message said that every package manager that had already published pointed at missing files. The v0.3.4 publish run (37166116245) shows otherwise: the jobs that published early (crates.io, the Homebrew formula and cask, both AUR packages) used macOS and Linux files that were already there, and the jobs that needed the Windows files (npm, MCP bundles, Scoop, winget) failed and were run again. What had nothing to download in the 1 h 38 min were install.ps1 and the in-app updater.
Two corrections from an independent review of this branch. The docs and workflow comments told the maintainer to allow deployments of release-signing from `main` and `v*` tags. Nothing that signs runs from a tag any more, and with no ruleset on `v*` tags, anyone who can push could push a `v` tag onto a commit whose workflow reads the signing secrets. They now say `main` only. Komac 2.16.0's existing-PR search (get_existing_pull_request.rs) has no state filter, so a PR a moderator closed also stops it, and the job then exits 0 without submitting. The comments and docs said it found "its own PR" and that there was nothing to fix. They now say what happens and how to resubmit by hand with --skip-pr-check.
Contributor
|
Site preview: https://pr-557.netscli-site-preview.pages.dev Built from 50947b0 with Production is unaffected: netscli.com is served from GitHub Pages via |
# Conflicts: # CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit fixes for CI, the release flow and the supply chain (
areas/ci-release.md, local only). The release docs are rewritten to match.The two Highs
Releases go public only once every file exists (H6).
publish-release.ymlis now three jobs:preparechecks the tag, that the run is on main, that the release is a draft, and that the changelog has the section.buildcallsrelease.ymlas a reusable workflow against the still-draft release.promoterunscheck-release-assets.mjsover every expected file,latest.jsonincluded, then writes the notes, publishes, and dispatchespublish.ymlandpages.yml.release.ymlno longer answersrelease: published, so promoting can't start a second build. A failed Windows signing leg leaves an invisible draft, which "Re-run failed jobs" can finish. With 0.3.4's outage state recreated, the asset check fails and names the 13 missing files.The winget token no longer reaches unpinned code (H5). Both winget jobs download Komac 2.16.0 and refuse it unless its SHA-256 matches the pinned value. Komac's own
SHA256SUMSand the GitHub API agree on that value. The token is only in thekomac update --submitandkomac cleanupsteps.Also fixed
requireSignedVersion). The manifest builder and the asset check refuse a signature with no version.tauri build --no-bundleruns with no secrets, thentauri bundleruns with them.persist-credentials: false.install.ps1requires a valid Authenticode signature onnetscli.exefrom the expected signer.install.shchecks the cosign identity when cosign is installed..mcpbbundles get checksums and signatures./scripts/mcp-smokeand/scripts.docs/RELEASE.mdanddocs/PUBLISHING.mdare rewritten to describe this flow. RELEASE.md gains a staged in-app update check.Corrections to the audit
install.ps1and the in-app updater.release-signingshould bemainonly, notmainplusv*tags. Nothing that signs runs from a tag any more, and with no tag ruleset avtag would be a way in.Checked
lib_test.shpasses, andnode --teston the release scripts passes 20 of 20.install_test.shpasses 11 of 11. 8 of those fail against the oldinstall.sh.version:0.3.5into the signature.check:changelogandcheck:structured-datapass in all three.Risk
promote, the release stays a draft, which is the point of the change.publish-preflightfirst.By hand, before publishing 0.3.5
release-signingdeployments tomainonly.v0.3.5tag to the merge commit, so 0.3.5 shipsrequireSignedVersion.fstubner/*, then rotateWINGET_TOKEN.v*tags.