Releases: sebastienrousseau/stratos
Stratos v0.0.21
Fixed — root help and macOS symlink-path entry guard (post-v0.0.20 verification)
Two latent issues surfaced during the v0.0.20 post-release verification pass. Neither affects any supported install path; both are tightenings to keep the surface honest.
- Root
stratos helpnow listscost,carbon, andrules validate. The v0.0.20 release added the commands and shipped per-command help (stratos help cost, etc.),stratos schema, and MCPtools/listentries — but the top-level help index was not updated. Discoverability for the FinOps differentiator was broken at exactly the surface a first-time user reads. Adds a dedicatedCost & sustainabilitysection toHELP_ROOTand slotsrules validatenext to itsget/set/diffsiblings. - Entry guard now realpath-canonicalizes
process.argv[1]before comparing againstfileURLToPath(import.meta.url). On macOS,/tmpis a symlink to/private/tmp; the ESM loader canonicalizesimport.meta.urlthrough realpath butargv[1]retains the raw path the user typed, so a directnode /tmp/.../stratos.mjs versionsilently exited 0 with no output. Same class as the v0.0.18 Bun$bunfsmismatch (v0.0.19 changelog). Supported install paths (npm bin shim, install.sh, brew, docker, scoop, winget) are all canonical end-to-end and were never affected; this hardens the rarely-used direct-Node-invocation path so it can't regress silently again.
Engineering notes
- New regression test exercises both fixes end-to-end through a symlinked directory so the symlink-path bug stays caught regardless of platform (
/tmpis a regular directory on Linux runners, so the v0.0.20 test matrix never had a chance of catching it). realpathSyncis imported fromnode:fs— same module already imported forcreateReadStream. No new dependency, no new bundle weight.
Stratos v0.0.20
Added — Feature Breadth tier-7 + tier-1 (the FinOps differentiator + a DX win)
Implementation plan (~/Drop/stratos-ip.md) Phase 3.1 + 3.2 + a Tier-1 entry.
stratos cost [--days N] [--zone Z] [--projected]— spend breakdown by region. Reads/api/billing/usagewhen CloudCDN exposes it (some deployments don't yet); falls back to projecting from/api/core/statistics× the published rate card (COST_RATES).--projectedforces the projection mode explicitly. Output columns:region,requests,bandwidth_gb,cost_usd. Surfaces as MCP toolcloudcdn_cost.stratos carbon [--days N] [--region X] [--intensity-below N]— energy + CO2e attribution for cached traffic. Computescompute_kwh + transfer_kwhper region from documented coefficients (CARBON_DEFAULTS), then multiplies by grid intensity. Live intensity comes from the Electricity Maps public API (free tier — setELECTRICITY_MAPS_TOKENfor the keyed tier); on any failure (network glitch, missing region, rate limit), falls back to documented per-region defaults so the command never hard-fails on a network blip.--intensity-below Nis the carbon-aware deploy gate: exits 0 if at least one region's grid intensity is below the threshold, 69 otherwise. Use as:stratos carbon --intensity-below 250 && stratos deploy. Surfaces as MCP toolcloudcdn_carbon.stratos rules validate <_headers|_redirects> -f <file>— offline structural validator for the two config-as-code formats. Catches the common typos (bad URL pattern, missing colon in header, malformed redirect status, wrong token count) at PR-review time instead of at deploy time. Exits 0 on clean, 65 (EX_DATAERR) with a line-by-line issue list otherwise. No API call.
Documentation
ROADMAP.md— new file mapping the 8 tiers of Feature Breadth work (existing-API completion → deployments → edge compute → edge data → security → identity → FinOps → compliance) against CloudCDN's platform-side API rollout. Tiers 2, 3, 4 are CloudCDN-confirmed roadmapped; the file tracks status transitions (planned→next→shipping→done) over time. Per the rating math in the implementation plan, completing all tiers lifts Feature Breadth from v0.0.20's ~6/10 to ~9.5/10 over 12–18 months.
Background
The implementation plan's Phase 3 called stratos cost + stratos carbon "the talkable feature that earns Console.dev / TLDR placement" — nobody else in the CDN-CLI space has shipped sustainability primitives at the terminal. The FinOps Foundation formalised Cloud Sustainability as a framework capability in 2026; Greenpixie / Opencost surface this for IaC but no CDN CLI does. v0.0.20 ships both. stratos carbon --intensity-below is the carbon-aware deploy gate that the byteiota — FinOps + GreenOps 2026 writeup specifically called out as "talked about, almost no CLI implements it."
The rules validate addition is a Tier-1 DX gap that didn't need a platform change — it caught me out twice in the v013-branch-push tests when typo'd _headers lines silently passed rules set then exploded at edge-time. Now they fail at validate time, before the file ever reaches the API.
Engineering notes
- 19 new tests (
test/v020-cost-carbon.test.mjs) covering happy path + auth-error + threshold-gate + region-fallback + zone-flag forwarding + MCP dispatch for both new tools. stratos schemanow reports 36 commands (was 34). Both new commands carrymcp_toolentries;costandcarbonare also registered inMCP_TOOLSwith proper input schemas so MCP hosts (Claude Code, Cursor) get them automatically.rulescommand's exits[] now includesEX.DATAERR(65); the manifest regression suite caught this drift on first run.- 603 / 603 tests pass; 100 / 100 / 100 / 100 coverage held (some defensive nullish-coalesce arms wear
c8 ignorecomments with explanations).
Stratos v0.0.19
Fixed
- Windows binary entrypoint guard — second attempt. v0.0.18 tried to detect the Bun-compiled binary context via
Bun.embeddedFiles.length > 0, but that array is for--embed FILEextras (additional files passed alongside the entry script), not the main entry itself. So the check stayedfalseon Windows,main()never ran, and the binary remained silent. The v0.0.18 release.yml's strengthened smoke step (assert stdout matches ^stratos v[0-9]) caught it cleanly: thebinaries (windows-latest, …, smoke: true)step failed with::error::empty or malformed output: '', every downstream job (manifests / SLSA / tap-bump / scoop-bump / winget-submit / smoke-verify) was skipped via theneeds:dependency chain, and no broken Windows binary was attached to the v0.0.18 release. npm @cloudcdn/stratos@0.0.18 and the Docker image v0.0.18 did publish (their paths don't go through the Bun binary). - v0.0.19's fix uses the simpler invariant:
typeof Bun !== 'undefined'. Stratos is a Node-only package (engines.node >= 20;NON-GOALS.mddocuments the no-Bun-as-library stance), so any time the Bun global exists at runtime, we're inside abun build --compileartefact. Library importers under Bun aren't a supported configuration; in the unlikely case someone tries,main()runs eagerly — a minor surprise rather than a silent-zero-exit, which is the right side of the trade-off. The Node-entrypoint check is unchanged. - The bug class that originally produced this — smoke step asserts only exit code, not output — was already fixed in v0.0.18. The strengthened assertion did its job during the v0.0.18 release: identified the silent binary, aborted the release before downstream artefacts shipped. That gate is what makes v0.0.19 a low-stakes iteration rather than a v0.0.16-style "ship and pray, fix later" cycle.
Partial v0.0.18 state to be aware of
- ✓
@cloudcdn/stratos@0.0.18published to npm (the fix code is there; works for npm users — Bun binary isn't involved on that path). - ✓ Docker image
ghcr.io/sebastienrousseau/stratos:0.0.18published (Docker runs Node). - ✓ GitHub Release v0.0.18 exists with canonical artefacts:
stratos.mjs, installers, SBOM, VEX, man page, four of the five binaries (linux-x64, linux-arm64, darwin-x64, darwin-arm64) and their Cosign signatures. ⚠️ Nostratos-win-x64.exeattached to the v0.0.18 release (the silent binary was rejected at smoke).⚠️ Homebrew tap, Scoop bucket, winget still pointing at v0.0.17 (themanifests/tap-bump/scoop-bump/winget-submitjobs wereneeds:-blocked by the failed binaries cell).⚠️ No SLSA L3 attestation bundle for v0.0.18 (same skip).
v0.0.19 will land all of the above end-to-end — assuming the new entrypoint check works (and the strengthened smoke is now the proof).
Stratos v0.0.18
⚠️ Superseded by v0.0.19v0.0.18 ships only a partial set of artefacts. The Windows binary smoke
(intentionally) blocked the release at thebinariesstep when the
entrypoint-guard fix didn't work, so:
- ✓
@cloudcdn/stratos@0.0.18did publish to npm (the fix code is
there; npm path doesn't involve the Bun binary)- ✓
ghcr.io/sebastienrousseau/stratos:0.0.18did publish (Docker
runs Node, not the Bun binary)⚠️ Nostratos-win-x64.exeattached here⚠️ Homebrew tap, Scoop bucket, winget all still point at v0.0.17⚠️ No SLSA L3 attestationv0.0.19 lands the correct fix end-to-end across every channel. Use
v0.0.19 or later.
Fixed
- Windows binary (
stratos-win-x64.exe) was silent on every invocation. v0.0.15 through v0.0.17 shipped abun --compile-built Windows binary that exited0while producing zero bytes of stdout for every command. Discovered by reproducingmicrosoft/winget-pkgs#382755— the reviewer @stephengillie's manual validation ofstratos --versionreturned empty output, and we narrowed the cause through two rounds of CI diagnostic (diag-windows-binary.yml,diag-windows-binary-2.yml).- Root cause: the script-entrypoint guard at the bottom of
stratos.mjsused the classic Node checkfileURLToPath(import.meta.url) === process.argv[1]. In a Bun-compiled standalone binary on Windows,process.argv[1]is the.exepath on disk (C:\…\stratos.exe) whileimport.meta.urlresolves into Bun's virtual$bunfsfilesystem (file:///$bunfs/root/…). These never match → guard false →main()never runs → silent zero-exit. Linux/macOS binaries happened to work because theirargv[1]/url comparison coincidentally passes, but the codepath was just as fragile. - Fix: detect compiled-binary context explicitly via
typeof Bun !== 'undefined' && Array.isArray(Bun.embeddedFiles) && Bun.embeddedFiles.length > 0and runmain()if either that OR the Node entrypoint check is true.Bun.embeddedFilesis non-empty only inside abun build --compileartefact, so importers ofstratos.mjsas a library (Node or Bun, from source) still seemain()not auto-run — the programmatic-API contract is preserved.
- Root cause: the script-entrypoint guard at the bottom of
- The release-pipeline binary smoke was a no-op gate.
shell: bash, ./<binary> versionwithset -esawexit 0from the silent binary and passed. Strengthened to assert the stdout matches^stratos v[0-9]explicitly — any future silent-zero-exit fails the release matrix at build time. smoke-verifymatrix now includesbinary-win-x64alongside the existingbinary-linux-x64andbinary-darwin-arm64cells. Mirrors what the winget reviewer's manual test did:curlthe.exefrom the release and verify the version banner. Closes the post-release verification gap that let v0.0.15..v0.0.17 ship a broken Windows binary unnoticed.
Background
PR microsoft/winget-pkgs#382755 was closed unmerged on 2026-06-13 by the Microsoft reviewer after manual validation. CloudCDN.Stratos couldn't be auto-resubmitted because (a) it's not yet in microsoft/winget-pkgs (so the auto-bump action skips by design) and (b) the underlying silent-output bug would have produced an identical failure for any new submission. v0.0.18 fixes the binary; a manual first-version PR for manifests/c/CloudCDN/Stratos/0.0.18/ (already pre-built and attached to the GitHub release as winget-manifests.tar.gz) is the next step to land CloudCDN.Stratos on winget.
Diagnostic workflows retained
diag-windows-binary.yml— round 1, walks the failure surface across shells and invocationsdiag-windows-binary-2.yml— round 2, discriminates Bun-build vs stratos.mjs vs stdio
Both are workflow_dispatch only — no auto-fire. Kept as record of the investigation and as ready-to-trigger probes if the silent-output class ever recurs.
Stratos v0.0.17
Added — schema-driven regression suite (Phase 0 follow-up)
- 50 new regression tests across three new files that pin the public surface as a contract:
test/v017-regression-manifest.test.mjs(15 tests) — schema/MCP_TOOLS/EX/error_types/exports cross-reference. Catches drift where a new command is added without updating one of the four parallel registries. EveryEX.*code must be referenced somewhere; everyMCP_TOOLSentry must map to a known command verb; everyerror_types.retryablebit is the documented contract.test/v017-regression-cli-smoke.test.mjs(20 tests) —stratos help <cmd>andstratos <cmd> --helpexit cleanly for every command inKNOWN_COMMANDS. Subcommand routers reject unknown subcommands withEX.USAGE.stratos completion <shell>renders for bash/zsh/fish/powershell.stratos explainworks for everyEXvalue, the documented symbolic aliases, and common HTTP statuses.stratos halthproduces a Levenshtein "did you mean health" suggestion.test/v017-regression-output-matrix.test.mjs(15 tests) — every documented--outputformat (json, ndjson, jsonl alias, yaml, csv, table) renders against a mock server for both single-object and list bodies.--jsonis verified as a shorthand for--output json.--filterjq pipeline works on single-line expressions.
Fixed
stratos help <cmd>now works for every command inKNOWN_COMMANDS, not just the ~24 with curated entries inHELP_BY_COMMAND. Falls back to a synthesisedstratos <cmd>\n <summary>\n Exit codes: <list>fromCOMMAND_METAwhen no hand-tuned entry exists. Closes the "stratos help stats / audit / analytics / ask / search / upgrade / logout / stream / pipeline / passkey returns EX.USAGE" surface that the regression suite caught on first run.--filternow handles all jq output shapes.applyFilter()parses each line of jq's stdout as an independent JSON value, so filters that produced pretty-printed multi-line single documents (e.g.{status, edge}) used to fail with "jq produced non-JSON output". Fixed by passing-c(compact) to jq — every line is now a complete JSON document, so single values, stream-style outputs (.[].id), and object projections ({a, b}) all work uniformly. Single-line expressions and per-line streams were already working; this just closes the multi-line gap. Discovered by the v017 regression-output-matrix suite on first run.- Bumps
EXPECTED_SHAininstall/install.{sh,ps1}to match the v0.0.17 bytes.
Stratos v0.0.14
Fixed
winget-submitno longer fails the release on a package that doesn't exist yet inmicrosoft/winget-pkgs.vedantmgoyal9/winget-releaser@v2is strictly a bump action — it errors out on the first version. v0.0.13's run failed for exactly this reason. v0.0.14 adds a pre-check that probesapi.github.com/repos/microsoft/winget-pkgs/contents/manifests/c/CloudCDN/Stratos: if the package isn't there yet, the step skips with a titled::warning::+ step-summary entry pointing at the one-time manual PR workflow. Once that PR merges, subsequent releases auto-bump as designed.- Test isolation race between
test/v006.test.mjsandtest/v007.test.mjs. Both calledscripts/make-winget.mjsandscripts/make-scoop.mjswriting todist/winget/anddist/stratos.scoop.jsonin the repo root. node--testruns files in parallel; the second write would clobber the first, and the first's assertions would fail on slower runners (Windows / Node 22 was the most frequent victim). Both scripts now accept a--dist-dir <dir>flag; both test files use per-testmkdtempto isolate. No more flake.
Changed
scripts/make-winget.mjsandscripts/make-scoop.mjsgain a--dist-dir <dir>option. Default behaviour (write todist/) unchanged; the flag is for callers that want to isolate.
Stratos v0.0.13
Verified
- First release where
tap-bump,scoop-bump, andwinget-submitactually drive the publish-side instead of skipping. No code changes from v0.0.12 — this is a version-bump-only release whose purpose is to exercise the now-configured secrets (HOMEBREW_TAP_TOKEN,SCOOP_BUCKET_TOKEN,WINGET_PAT) end-to-end. Confirmation criteria: the v0.0.13 GH Release run shows actual git pushes tosebastienrousseau/homebrew-tapandsebastienrousseau/scoop-bucketand an actual PR opened againstmicrosoft/winget-pkgs(instead of three::warning::skips).
Stratos v0.0.12
Fixed
smoke-verify (npm)was the last false-failure cell. v0.0.11's$(npm config get prefix)/bin/stratosreturned a path that didn't actually contain the symlink on Ubuntu runners —got:came back empty. Rewritten to run the just-installedstratos.mjsdirectly vianode "$(npm root -g)/@cloudcdn/stratos/stratos.mjs" version. Points at the file npm wrote to disk; no PATH / symlink dependency.install.shandinstall.ps1now default to the GitHub Release URL (https://github.com/sebastienrousseau/stratos/releases/download/v<version>/stratos.mjs) instead ofhttps://cloudcdn.pro/dist/stratos/stratos.mjs. The CDN is now opt-in via theCLOUDCDN_URLenv var. The GH release is canonical (signed by the release workflow, immutable per-tag, SLSA L3 attested) and removes the dependency on a separatecdn-syncstep having credentials configured. Thesmoke-verify (install-sh)fallback that worked around the stale CDN can be removed in v0.0.13.
Stratos v0.0.11
Added — validation hardening
actionlintCI gate (newlint-workflowsjob inci.yml). Every PR that touches.github/workflows/*.ymlis now parsed byactionlint+ its embeddedshellcheck. This would have caught the v0.0.10 / v0.0.11 ghost-tag bug (multi-linegit commit -m "…"strings breaking YAML block-scalar indentation) at PR-review time, before any tag could fire a broken workflow.scripts/check-versions.mjs— single source of truth that verifies every version-bearing file agrees:stratos.mjs VERSION,package.json version,package-lock.json version,install.sh VERSION,install.ps1 $Version,test/router.test.mjsassertions, topCHANGELOG.mdentry. Also assertsinstall.sh EXPECTED_SHA(and the PowerShell equivalent) matches the actual SHA-256 ofstratos.mjs. Wired into a newcheck-versionsCI job. This would have caught the v0.0.6 EXPECTED_SHA-drift bug and the v0.0.11 branched-off-stale-main bug.scripts/preflight-release.sh— mandatory pre-git tagchecklist that runs locally: workflow YAML parses,actionlintclean,check-versionsclean, working tree clean, local main is up to date with origin/main, new version is strictly greater than the latest tag, CHANGELOG has an entry for the new version,npm test+coverage:check+docs:checkall green. Run it before every tag.
Fixed
smoke-verifymatrix bugs that caused 3/6 channels to fail on v0.0.10:- npm smoke now uses
$(npm config get prefix)/bin/stratosinstead of barestratos— bypasses the Ubuntu-runner PATH issue that returned emptygot:. - install-sh smoke no longer depends on
cloudcdn.probeing fresh. The smoke step downloadsstratos.mjsfrom the GH release directly when the CDN's content doesn't match, so a skippedcdn-syncno longer breaks the smoke. - homebrew smoke first reads the tap's currently-published version and short-circuits with a
::warning::(not a failure) if it's behind — distinguishes "tap-bump was skipped because HOMEBREW_TAP_TOKEN is unset" from "we shipped something broken".
- npm smoke now uses
- Gate-skip semantics for
tap-bump/scoop-bump/winget-submit/cdn-sync. Each gated step now emits a titled::warning::annotation and a### ⚠️ SKIPPEDsection in the workflow's Job Summary. This makes the difference between "step succeeded by skipping" and "step succeeded by actually doing the work" visible at-a-glance in the GitHub Actions UI. - shellcheck warnings in
release.yml—sha256sum *→sha256sum ./*(guards against--prefixed filenames), and 4 × SC2129 (grouped redirects to$GITHUB_STEP_SUMMARY).actionlintnow exits 0.
Stratos v0.0.10
Added
- Five new distribution channels wired into the release pipeline. The
release.ymlworkflow now auto-bumpssebastienrousseau/homebrew-tap(gated onHOMEBREW_TAP_TOKEN), pushes tosebastienrousseau/scoop-bucket(gated onSCOOP_BUCKET_TOKEN), opens a PR againstmicrosoft/winget-pkgsviavedantmgoyal9/winget-releaser@v2(gated onWINGET_PAT), and uploadsstratos.mjs+ installers tocloudcdn.pro(gated onCDN_UPLOAD_URL+CDN_UPLOAD_TOKEN). Every gate is graceful — missing secrets emit a::warning::and skip the step, leaving the core release green. - Post-release smoke verification job. New
smoke-verifymatrix runs after every release, installs Stratos from npm + Homebrew + Docker + linux-x64 binary + darwin-arm64 binary + the GH-release-hostedinstall.sh, runsstratos version, and fails the workflow if any channel prints anything other than the expected tag. Catches "shipped but doesn't install" regressions (the class of bug that bit v0.0.7'sinstall.shon BSD-stylesha256sum). docs/release-pipeline.md— full job-graph diagram, secret-by-secret setup instructions, and the bootstrapping recipe for adding new distribution channels (Arch AUR, Snap, Nix flake, etc.).
Fixed
- README install table fully expanded and corrected. Eight distribution channels (npm, Homebrew, winget, Scoop, single binary, install.sh, install.ps1, from-source) instead of five. The Homebrew command uses the fully-qualified
brew install sebastienrousseau/tap/stratos— barebrew install stratosdoes not find tap-only formulas, which we confirmed by smoke-testing the v0.0.9 tap end-to-end during scoping.