kylon-cli 0.6.0-next.869 → 0.7.0-next.873

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -432,6 +432,114 @@ trigger, not a required extra step after a production deploy. Do not run
432
432
  Development installs `@next` and production installs `@latest`, so each
433
433
  environment tracks its own release train.
434
434
 
435
+ #### Auto-update policy: dev is automatic, production is manual
436
+
437
+ The two `cli_auto_update` keys are maintained by deliberately different
438
+ mechanisms.
439
+
440
+ **`cli_auto_update/next` (dev) is reconciled automatically.** After a `develop`
441
+ prerelease is published, the `sync-dev-auto-update` job in
442
+ `.github/workflows/cli-publish.yml` sets the dev entry to the exact version it
443
+ just released, with `enabled: true` and `rolloutPercentage: 100`. A staged
444
+ rollout on dev would serve no purpose: the prerelease it points at is the only
445
+ build `@next` resolves to, so anyone tracking `next` is getting it regardless.
446
+
447
+ The job verifies that `kylon-cli@next` actually resolves to the published
448
+ version before writing (retrying for registry read lag), and writes through
449
+ `upsertRuntimeConfigEntry`, so the same schema validation and
450
+ `runtime_config_entry_revisions` audit row apply as for an admin edit. The
451
+ revision's change reason records the published version, commit and workflow run.
452
+
453
+ The write is fenced with the entry version the job read, so it is a
454
+ compare-and-swap rather than a blind overwrite. That matters because
455
+ `cli-publish.yml` deliberately gives every run its own concurrency group: two
456
+ `develop` prereleases can be in flight at once and both reach this reconciler.
457
+ The rule is **latest wins** — the highest `X.Y.Z-next.N` the job can observe,
458
+ whether on the dist-tag or already stored in the entry, owns the target. An
459
+ older run that sees a newer one records a `superseded` no-op and succeeds; if
460
+ its fenced write loses the race instead, it re-reads both the dist-tag and the
461
+ row and re-decides against the state that won. An older release can therefore
462
+ never overwrite a newer one, and the entry never flaps between two concurrent
463
+ runs.
464
+
465
+ **The dist-tag is part of the same convergence, not an input to trust.**
466
+ `npm publish --tag next` moves the tag by wall clock, not by version, so two
467
+ overlapping runs that finish in reverse order leave the registry serving the
468
+ *older* build: run 811 publishes, then run 810 publishes and takes `@next`.
469
+ That is user-visible — `automatic-cli-update.ts` refuses to upgrade a machine
470
+ whose configured target differs from the dist-tag, logging `release_mismatch` —
471
+ so the two surfaces have to agree on one canonical winner: the newest
472
+ `X.Y.Z-next.N` any run can observe on its own release, on the tag, or in the
473
+ row.
474
+
475
+ Two mechanisms keep them agreeing:
476
+
477
+ - **The publish never moves `@next` backwards.** Immediately before publishing,
478
+ the run re-reads the dist-tag; if it already serves a strictly newer
479
+ prerelease, this run is already superseded and publishes under the
480
+ non-promoting `next-superseded` tag instead. The version is still published
481
+ and still installable by exact version — it just does not take the channel
482
+ head. A lookup that cannot be answered stops the run *before* the
483
+ irreversible step rather than publishing blind.
484
+ - **The sync repairs what is left.** A check-then-publish window remains, so if
485
+ the sync job finds `@next` behind the canonical winner it moves the tag
486
+ first — under a re-read fence, so an older run can never pull it back — and
487
+ only then writes the row. The row is never pointed at a version the dist-tag
488
+ does not serve, because that state upgrades nobody.
489
+
490
+ Moving a dist-tag needs a credential: npm's OIDC trusted publishing
491
+ authenticates `npm publish` and nothing else ([npm/cli#8547](https://github.com/npm/cli/issues/8547)).
492
+ The sync job reads an optional `NPM_DIST_TAG_TOKEN` from its `dev`
493
+ environment; when it is absent, the run fails with an `::error::` naming the
494
+ exact `npm dist-tag add kylon-cli@<winner> next` to run, instead of reporting
495
+ a convergence that did not happen.
496
+
497
+ Propagation is not part of the write's success condition. Committing the entry
498
+ also bumps a per-namespace revision counter and publishes a cache-invalidation
499
+ message on Redis, but no runtime reader polls that counter: services drop their
500
+ in-process runtime-config snapshot when the pub/sub message arrives, and
501
+ otherwise when the snapshot ages past its five-minute TTL. The publish is
502
+ bounded inside `invalidateRuntimeConfig`, so an unreachable Redis (the CI job
503
+ tunnels Postgres only) costs at most that five minutes of staleness and cannot
504
+ stall or fail an already-committed, already-audited write.
505
+
506
+ **`cli_auto_update/latest` (production) is never touched by automation.** The
507
+ stable target version and its rollout percentage are set by a human in the
508
+ admin runtime config, and ramping 0 → 100% is an explicit blast-radius
509
+ decision. Publishing a stable build to `@latest` does not enrol any computer in
510
+ updating to it; that remains a separate, deliberate step.
511
+
512
+ Two consequences worth knowing:
513
+
514
+ - **A dedup-skipped prerelease changes nothing.** When content dedup decides the
515
+ bundle is identical to what `@next` already has, no version is published and
516
+ the dev policy is left exactly as it was — the existing target is still the
517
+ build `@next` serves. The sync never fabricates a version for a release that
518
+ did not happen.
519
+ - **A failed sync fails the run, loudly, and re-running repairs it.** The npm
520
+ publish is irreversible, so the sync cannot be retried by re-publishing. If
521
+ the config write fails (or `@next` does not converge on the published
522
+ version), the workflow fails with an `::error::` naming the manual fix — a red
523
+ run, never a silent partial release.
524
+
525
+ "Re-run all jobs" is the supported repair. A re-run keeps the same run number
526
+ and therefore mints the same `X.Y.Z-next.<run>` version, which is already on
527
+ npm; the publish job detects exactly that case and marks the run
528
+ `reconcile_only`, so it publishes nothing but still runs the sync job. The
529
+ reconciliation is idempotent and reads the live dist-tag, so it either repairs
530
+ the stale entry, finds it already correct, or finds that a newer prerelease
531
+ has superseded this one and leaves the newer target alone. That is distinct
532
+ from an ordinary content-dedup skip, which publishes nothing and reconciles
533
+ nothing. If the re-run also fails, set `cli_auto_update/next` in the dev admin
534
+ runtime config by hand.
535
+
536
+ The classifier that tells those two skips apart is
537
+ `scripts/ci/cli-prerelease-npm.sh`, and every registry lookup in it retries
538
+ and fails closed. A transient `npm view` error is not evidence that a version
539
+ is unpublished; treating it as such is what would let a failed reconciliation
540
+ hide behind a green re-run. The dist-tag's own version is used as a second,
541
+ independent confirmation of the same fact.
542
+
435
543
  ## Usage
436
544
 
437
545
  ### Sign in for workspace commands
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.0-next.869",
3
- "fingerprint": "afb57efce175ef04f8fa6a2c4b3c7bd4066070f5c64fda3dad261167f7db7556",
4
- "source_commit": "f3c83f68293b19a61bac854d9ff40421231a5b25"
2
+ "version": "0.7.0-next.873",
3
+ "fingerprint": "1e155dfa2a8b6676fcf6d7fe7d69cd4cd0ff6b725e9af48383f0ae4e47151b83",
4
+ "source_commit": "b0c31dbf9e8413ecb155cb8693e1170eb6cf99d5"
5
5
  }