From eb44df3c11c7176fc6a68b6bcb8e5bdd511ad08f Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 09:09:38 +0200 Subject: [PATCH 01/14] docs(deploy): [Deploy-S6] research + plan for Deno Deploy tier-1 adapter (#342) Planning-only harness run docs (research.md + plan.md) for the Deno Deploy marquee target (D2). Grounds against #337 deploy.targets.* contract and #338 DeployTargetPort archetype; enumerates 4 commit slices, change map, fitness gates, and open decisions (CLI-push default, CI auth) as NEEDS USER. Refs #342 #327 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012wKHquACkXnWPDgJYhhFjN --- .llm/tmp/run/deploy-s6-deno-deploy/plan.md | 241 ++++++++++++++++++ .../tmp/run/deploy-s6-deno-deploy/research.md | 135 ++++++++++ 2 files changed, 376 insertions(+) create mode 100644 .llm/tmp/run/deploy-s6-deno-deploy/plan.md create mode 100644 .llm/tmp/run/deploy-s6-deno-deploy/research.md diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/plan.md b/.llm/tmp/run/deploy-s6-deno-deploy/plan.md new file mode 100644 index 000000000..1ecd125bf --- /dev/null +++ b/.llm/tmp/run/deploy-s6-deno-deploy/plan.md @@ -0,0 +1,241 @@ +# Plan: Deno Deploy tier-1 adapter (#342 [Deploy-S6], MARQUEE) + +## Run Metadata + +| Field | Value | +| -------------- | --------------------------------------------------------------- | +| Run ID | `deploy-s6-deno-deploy` | +| Branch | `feat/deploy-s6-deno-deploy` (off origin/main `cf1ac47b`) | +| Phase | `plan` (PLAN-ONLY; Implement blocked on #337 + #338 + PLAN-EVAL) | +| Target | `packages/cli` (adapter + wiring) + `packages/config` (schema) | +| Archetype | `6 - CLI/Tooling` (primary); folds `2 - Integration`; config member follows `1 - Small-Contract` | +| Scope overlays | `none` (no Fresh/UI; no new long-running service; Aspire-optional at runtime gate) | + +## Archetype + +**Primary: ARCHETYPE-6 (CLI / Tooling).** The deliverable ships a user-run command flow in the +`@netscript/cli` package (`netscript deploy deno-deploy `). Per the archetype decision order, +when a package "ships a binary or command-line flow" it is Arch-6, and smaller concerns fold inside. + +**Folded: ARCHETYPE-2 (Integration).** The substance is wrapping an external system — the Deno +Deploy platform via the `deno deploy` CLI — behind a package-owned port. #338 already provides that +port (`DeployTargetPort`) and registry (`DeployTargetRegistry`); the deno-deploy adapter is a new +**adapter on an existing axis**, exactly the Arch-2 "named adapter behind an owned port" shape. It +must shell the external CLI through a `ProcessPort` (mirroring `ServyCliAdapter`), never ad-hoc +`Deno.*` in presentation/application layers (F-CLI-5/16, AP-11). + +**Config member: ARCHETYPE-1 (Small-Contract) sub-concern.** The `deploy.targets['deno-deploy']` +schema member is a types+schema addition to the published `@netscript/config` package, following the +exact spread-composition + `z.ZodType` pattern #337 established for `windows` (Findings 1–2). + +Justification for not splitting: doctrine says "if two archetypes apply, choose the larger and fold +the smaller." The CLI flow is the marquee; integration and the small contract are folded. The config +schema lives in a *different published package* (`@netscript/config`), so its slice carries the +Arch-1 publishability gates (F-6 / publish dry-run) while the CLI slices carry the F-CLI-* family. + +## Current Doctrine Verdict + +N/A as a fresh verdict for this consumer-of-doctrine slice. The governing doctrine is +`07-composition-and-extension.md` (Deploy target axis; `registerDeployTarget` factory pattern; no +cross-package inheritance; every `Registry` in `extension-points.ts`) as amended on +`feat/deploy-s2-doctrine` (#338). The adapter conforms to that axis; it does not change doctrine. + +## Axioms in Play + +| Axiom | Why it matters | +| ----- | -------------- | +| A2 (public surface minimal) | Adapter is internal; only the config type + schema is new public surface. | +| A5 / A6 (ports & adapters, no cross-boundary inheritance) | Adapter implements `DeployTargetPort`; consumer supplies a factory, not a subclass (doctrine R-COMP). | +| A11 (edge-only side effects) | `deno deploy` shelling goes through a `ProcessPort` in `adapters/`; no `Deno.Command` in features/presentation (F-CLI-16, AP-11). | +| A8 (extension via typed registry) | New target registered in `DeployTargetRegistry`, exported from `extension-points.ts` (F-CLI-31, AP-24). | +| A13/A14 (declarative composition, documented permissions) | Composition declarative (F-CLI-27); README declares `--allow-run` for `deno deploy` + network (AP-19). | + +## Goal + +Add a tier-1 **Deno Deploy** deployment target to NetScript: a `deploy.targets['deno-deploy']` +config member (extends #337's base) + a resolver + a `DenoDeployTarget` adapter implementing #338's +`DeployTargetPort`, which shells the native `deno deploy` CLI (source build, no Dockerfile) to push a +scaffolded NetScript project to the new Deno Deploy platform — reachable via the CLI as +`netscript deploy deno-deploy `. Includes the **unstable-API preflight guard** (Finding 10). + +## Scope + +- `packages/config`: `DenoDeployTarget` interface + `DenoDeployTargetSchema` (`z.ZodType<…>`, + spread-composed on `deployTargetBaseShape`); add `deno-deploy` key to `DeployConfigSchema.targets` + and `DeployConfig.targets`. +- `packages/cli` kernel: `DenoDeployTarget` adapter implementing `DeployTargetPort` + (`key='deno-deploy'`), registered in `DEFAULT_DEPLOY_TARGETS`; a `deno deploy` process wrapper + adapter (mirrors `ServyCliAdapter`); an unstable-API preflight checker; a + `resolveDenoDeployTarget` config resolver. +- `packages/cli` CLI reach: expose the deno-deploy target's operations through the deploy command + surface (minimal, register-first; see Locked D6), with `--org`/`--app`/`--prod`/`--env-file` and + `--dry-run` options mapped onto `deno deploy` flags. +- Tests: schema round-trip/defaults; resolver defaults; adapter unit tests with a fake `ProcessPort` + (asserting the exact `deno deploy` argv, exit-code mapping, and unstable-guard refusal); registry + membership test. +- Docs: README permissions block (`--allow-run`, network) + a short how-to note; drift/worklog. + +## Non-Scope + +- Merging #337/#338 (external prerequisites; this slice rebases onto them). +- Managed Postgres/KV wiring (`deno deploy database`), HA/multi-region, rollback/promote/traffic + verbs, GitHub-push auto-build provisioning, Deploy Classic/deployctl — all deferred (research + Non-goals). +- Generalizing windows-only lifecycle verbs (start/stop/upgrade/copy/package-cli) to the cloud target + — no cloud analogue; beta implements only `DeployTargetPort` operations. +- Full `netscript deploy ` router refactor of the windows-hardcoded `deploy-group.ts` + — that generalization is #338/epic router work; this slice reaches the target through the registry + with the smallest necessary CLI surface (Locked D6). + +## Hidden Scope + +- **Operation-vocabulary mismatch (Finding 3 vs corpus).** #338's port exposes only + `build|install|uninstall`; Deno Deploy's natural verb is "deploy/push". The plan maps semantics + onto the existing 3 operations (Locked D3) rather than editing #338's port, to avoid cross-slice + coupling. If the port owner adds a `deploy` operation, revisit. +- **`isolatedDeclarations` on config** — the new interface needs explicit field types; the schema + must keep the `z.ZodType` annotation or fast-types/`deno doc` break (Finding 2). +- **Unstable-API guard is not optional** — without it the marquee "one-click" silently ships an app + that dies on-platform (KV `--unstable-kv`). This is a first-class slice, not a nice-to-have. +- **Windows key precedent** — keep config member key, CLI token, and registry key all `deno-deploy` + (Finding 14) to avoid the windows `windows` vs `windows-service` split. + +## Locked Decisions + +| ID | Decision | Rationale | +| -- | -------- | --------- | +| D1 | **Implement phase depends on #337 merged/rebased first.** Plan proceeds now against the read contract; no config code committed until the `deploy.targets.*` base exists on the working base. | #337 owns `DeployTargetBaseSchema`/`DeployConfigSchema` this member extends (Findings 1–2). | +| D2 | **Adapter conforms to #338's `DeployTargetPort`/`DeployTargetRegistry`; Implement depends on #338 merged/stabilized.** Design against the read port now. | #338 owns the target-adapter archetype (Findings 3–5); PLAN-EVAL checks conformance. | +| D3 | **Operation mapping:** `build` = preflight (validate `deno.json` Deploy build settings + run the unstable-API guard, no remote call); `install` = **push/deploy** via `deno deploy` (`--prod` gated by an option); `uninstall` = delete the app/deployment. Conform to the existing 3-op port; do **not** edit #338's port in this slice. | Mirrors windows (`install`=create, `uninstall`=remove) and avoids cross-slice port coupling. Marquee "deploy" is surfaced as a CLI verb aliasing `install` (see D6). | +| D4 | **CLI push is the beta driver** (`deno deploy` from project root), not GitHub-push auto-build. GitHub-push is deferred. | Shortest deterministic one-click for a CLI tool; matches D2 "shortest path" marquee intent. *(Product-facing default — also flagged in Open-Decision sweep for user confirmation.)* | +| D5 | **Shell `deno deploy` through a `ProcessPort` adapter** (new `DenoDeployCliAdapter`), mirroring `ServyCliAdapter`; capture stdout/stderr, map exit code → `DeployTargetResult`. No `Deno.Command` outside `kernel/adapters/**`. | Arch-6 R-A6-N8 / F-CLI-16; Arch-2 named-adapter-behind-port; testable with a fake port. | +| D6 | **Register-first, minimal CLI reach.** Add the target to `DeployTargetRegistry` (authoritative) and expose it through the deploy command surface with the smallest change that lets `netscript deploy deno-deploy ` run; do **not** refactor the windows-hardcoded `deploy-group.ts` router. | Keeps the slice bounded; the full `deploy ` router is epic/#338 work (research Finding 6). | +| D7 | **Registry key = CLI token = config member key = `deno-deploy`.** | Avoid the windows `windows`/`windows-service` split (Finding 14). | +| D8 | **Unstable-API preflight guard is a required slice.** `build`/`install` scan the entrypoint + `deno.json` imports for `--unstable-*`-requiring APIs (KV et al.) and refuse (or hard-warn) before push. | Deno Deploy rejects `--unstable-*`; NetScript KV plugins use `--unstable-kv` (Finding 10). | + +## Open-Decision Sweep + +| Decision | Status | Notes | +| -------- | ------ | ----- | +| Op-vocabulary: reuse `build/install/uninstall` vs add a `deploy` op to #338's port | **safe to defer** (resolved as D3 for beta) | Deferring does not force rework: D3 conforms to the current port; adding a `deploy` op later is additive and coordinated with #338's owner. | +| **CLI-push vs GitHub-push default** | **NEEDS USER** (proceeding on D4 = CLI-push) | Product-facing default for the marquee. D4 is a reversible assumption; if user wants GitHub-push-first, the driver changes but the port/schema/guard do not. | +| **Non-interactive CI auth for new `deno deploy`** (env var / org token) | **NEEDS USER** + freshness re-check at Implement | Docs confirm keyring + interactive prompt + user/org tokens but not a documented non-interactive env var (Findings 11–12). Blocks CI/e2e automation of a *real* push, not the adapter/unit design. Beta may ship CLI-push with an interactive-auth caveat + a `--token`/env passthrough pending confirmation. | +| Unstable-guard = **refuse** vs **warn-and-continue** | safe to defer (default: refuse for `--prod`, warn for preview) | Reversible flag on the guard; does not affect its structure. | +| Whether beta exposes `env`/`logs` passthrough verbs | safe to defer | Additive CLI verbs; out of the D6 minimal reach unless trivial. | + +> No open decision would force rework if deferred: the two `NEEDS USER` items affect the *driver +> default* and *CI auth wiring*, not the port conformance, schema shape, adapter structure, or guard — +> which are all locked. Recorded so PLAN-EVAL can confirm the deferral is safe. + +## Change Map + +| Area | File (relative to repo root) | Change | Slice | +| ---- | ---------------------------- | ------ | ----- | +| Config type | `packages/config/src/domain/config-section-types.ts` | Add `DenoDeployTarget extends DeployTargetBase`; add `deno-deploy?` to `DeployConfig.targets`. | S1 | +| Config schema | `packages/config/src/domain/schemas/deploy-schema.ts` | Add `DenoDeployTargetSchema: z.ZodType` (spread `deployTargetBaseShape` + Deno-Deploy fields); add `'deno-deploy'` key to `DeployConfigSchema.targets`. | S1 | +| Config test | `packages/config/**/deploy-schema*_test.ts` (co-located) | Round-trip + defaults + optionality for the new member. | S1 | +| Adapter port impl | `packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts` | `class DenoDeployTarget implements DeployTargetPort` (`key='deno-deploy'`, ops `['build','install','uninstall']`, D3 mapping). | S3 | +| Process wrapper | `packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts` | `DenoDeployCliAdapter` shelling `deno deploy` via `ProcessPort` (mirrors `ServyCliAdapter`); argv builders for create/deploy/env/uninstall. | S3 | +| Unstable guard | `packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts` (or `adapters/…` if it reads FS) | Scan entrypoint + `deno.json` for `--unstable-*`-requiring APIs; return a violation report. | S2 | +| Registry wiring | `packages/cli/src/kernel/application/registries/deploy-target-registry.ts` | Add `DENO_DEPLOY_TARGET` to `DEFAULT_DEPLOY_TARGETS`. | S3 | +| Extension-points | `packages/cli/src/kernel/extension-points.ts` | No new export needed (registry already exported); confirm re-export completeness (F-CLI-31). | S3 | +| Config resolver | `packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts` | Add `resolveDenoDeployTarget(userDeploy?.targets?['deno-deploy'])` → `ResolvedDenoDeployConfig` + defaults. | S4 | +| CLI reach | `packages/cli/src/public/features/deploy/**` (minimal, D6) | Surface `deno-deploy` operations + `--org/--app/--prod/--env-file/--dry-run` options mapped to `deno deploy` flags. | S4 | +| Adapter tests | co-located `*_test.ts` under the new folders | Fake `ProcessPort`: assert exact argv, exit-code→result, guard refusal, registry membership. | S2–S4 | +| README/permissions | `packages/cli/README.md` (permissions block) | Declare `--allow-run` (deno deploy) + network; note unstable-API caveat + auth caveat. | S4 | +| Debt note | `.llm/harness/debt/arch-debt.md` §`cli-deploy-artifacts-missing` | Update: cloud target added; debt advanced, not closed. | S4 | + +## Commit Slices + +Ordered per Arch-6 "kernel domain + ports + abstracts first; registries + adapters next; features +one at a time; composition last." Each is independently committed, pushed, PR-commented. All slices +are gated behind D1/D2 (rebase onto merged #337 + #338) at Implement time. + +| # | Slice | Proves | Gate | Files | +| - | ----- | ------ | ---- | ----- | +| S1 | **Config member** — `DenoDeployTarget` type + `DenoDeployTargetSchema` + `targets['deno-deploy']`. | The published config surface accepts a valid deno-deploy target and rejects bad shapes; fast-types intact. | `deno task check` (config, `--unstable-kv` where needed) + config `_test.ts` + `deno publish --dry-run` (config) + `run-deno-check.ts --root packages/config --ext ts`. | config type + schema + test. | +| S2 | **Unstable-API guard** (pure/kernel) + tests. | Guard flags `--unstable-*`-requiring imports (KV) in a fixture project and passes a clean one. | `run-deno-check.ts --root packages/cli` + guard `_test.ts` (fixtures) + fitness F-CLI-1/16 manual. | `unstable-api-guard.ts` + test. | +| S3 | **Adapter + process wrapper + registry** — `DenoDeployTarget implements DeployTargetPort`, `DenoDeployCliAdapter` (ProcessPort), `DEFAULT_DEPLOY_TARGETS` entry. | Adapter emits the exact `deno deploy` argv, maps exit codes, refuses on guard violation; registry lists `deno-deploy`. | adapter/registry `_test.ts` with fake `ProcessPort` + `run-deno-check.ts` + fitness F-CLI-16/17/19/28/31 manual + F-4 (kernel≠surface imports). | adapter, cli-wrapper, registry, extension-points confirm. | +| S4 | **Resolver + CLI reach + docs/debt** — `resolveDenoDeployTarget`, minimal deploy-surface reach, README permissions, arch-debt update. | `netscript deploy deno-deploy build` runs the guard/preflight against a scaffold without a real push; options map to `deno deploy` flags; README declares perms. | resolver `_test.ts` + CLI help/name check + `deno task lint` + `run-deno-fmt.ts --root packages/cli --ext ts` + fitness F-CLI-26/27 + AP-19 manual. | resolver, deploy-surface reach, README, arch-debt. | + +Slice count: **4** (< 30). If S3 reveals the #338 router must change to reach the target at all, +that is a **rescope trigger** (Arch-6 "command vocabulary / surface boundary change") → confirm with +user before expanding. + +## Risk Register + +| Risk | Mitigation | +| ---- | ---------- | +| #337/#338 not merged when Implement starts. | D1/D2 hard-block Implement on merge/rebase; PLAN designs against read contracts only. | +| #338's `DeployTargetPort` shape shifts before merge. | Design against the read interface; add a drift-watch entry; re-verify the port at Implement start. | +| Deno Deploy CLI surface/auth changes (fast-moving platform). | Freshness re-check (`deno deploy --help`) at Implement; auth flagged `NEEDS USER`; adapter isolates argv construction in one wrapper for cheap updates. | +| Unstable-API guard false-negatives (misses a transitive KV import). | Scan entrypoint + `deno.json` import map + (best-effort) `deno info` graph; document the guard's bounds; default to **refuse** on `--prod`. | +| Real e2e push needs live Deno Deploy creds/org. | Unit-test with fake `ProcessPort` (no live push); a real push is an interactive/manual verification, not a CI gate this slice, pending the auth `NEEDS USER`. | +| Scope creep into the full `deploy ` router. | D6 register-first minimal reach; router refactor is an explicit rescope trigger. | + +## Anti-Patterns to Resolve or Avoid + +| AP | Status | Plan | +| -- | ------ | ---- | +| AP-11 (side effects outside adapters) | risk | `deno deploy` shell only in `kernel/adapters/deno-deploy/**` via `ProcessPort` (D5). | +| AP-24 (switch-over-union for variants) | avoid | Register `deno-deploy` in the typed `DeployTargetRegistry`; no `if target === …` branching. | +| AP-3 (port with every backend op) | avoid | Conform to the existing 3-op `DeployTargetPort`; do not bloat it (D3). | +| AP-19 (undocumented permissions) | new | README permissions block: `--allow-run` + network + unstable/auth caveats (S4). | +| AP-1 (command/pipeline monolith) | avoid | Adapter + wrapper + guard + resolver are separate files, each ≤ per-layer LOC caps (F-CLI-1/2). | +| AP-18 (string-snapshot tests) | avoid | Assert semantic argv arrays + exit-code mapping, not stdout snapshots. | + +## Fitness Gates + +| Gate | Required | Expected evidence | +| ---- | -------- | ----------------- | +| Static (check/lint/fmt via scoped wrappers) | yes | `run-deno-check.ts`/`run-deno-lint.ts`/`run-deno-fmt.ts` on `packages/config` + `packages/cli`, `--ext ts`. | +| F-1/F-CLI-1,2 (file size) | yes | Adapter ≤ 350, guard/domain ≤ 250, command ≤ 150 LOC — manual/PENDING_SCRIPT. | +| F-4/F-CLI-4 (kernel ≠ surface imports) | yes | Import-graph manual: adapter in kernel imports no `public/**`. | +| F-CLI-16 (`Deno.Command` only in adapters) | yes | Regex/manual: shelling only in `kernel/adapters/deno-deploy/**`. | +| F-CLI-31 (registry in extension-points) | yes | `DeployTargetRegistry` already exported; confirm. | +| F-5 (public surface audit) | yes | Only `DenoDeployTarget`/schema newly public (config); CLI adapter internal. | +| F-6 / publish dry-run (config) | yes | `deno publish --dry-run` on `@netscript/config` (published surface). | +| F-9 / AP-19 (permissions declared) | yes | README permissions block updated. | +| F-10 (test-shape) | yes | Fake-`ProcessPort` semantic tests, guard fixtures, schema round-trip. | +| Runtime/Aspire (optional) | at evaluator | `scaffold.runtime` e2e is the merge-readiness authority (below) — evaluator/OpenHands, not per-slice. | + +## Arch-Debt Implications + +| Entry | Action | Notes | +| ----- | ------ | ----- | +| `cli-deploy-artifacts-missing` (`arch-debt.md:1300`) | update | Cloud (Deno Deploy) target added; debt **advanced not closed** (no Dockerfile/k8s/compose generation; windows + deno-deploy only). | +| (new, if guard bounds are partial) | create-if-needed | If the unstable-API guard is best-effort (misses deep transitive imports), record the bound as an explicit debt entry rather than claiming full coverage. | + +## Validation Plan + +| Order | Gate | Command or check | Expected result | +| ----- | ---- | ---------------- | --------------- | +| 1 | Config check | `deno run -A .llm/tools/run-deno-check.ts --root packages/config --ext ts` | 0 errors | +| 2 | Config tests | `deno test` on config deploy-schema `_test.ts` | green | +| 3 | Config publish surface | `deno task publish:dry-run` (config member) | dry-run success (modulo declared JSR_DEPS_PENDING) | +| 4 | CLI check | `deno run -A .llm/tools/run-deno-check.ts --root packages/cli --ext ts` (+`--unstable-kv` if KV-touching) | 0 errors | +| 5 | CLI tests | `deno test` on new adapter/guard/resolver `_test.ts` (fake `ProcessPort`) | green | +| 6 | Lint + fmt | `run-deno-lint.ts` + `run-deno-fmt.ts` on both roots, `--ext ts` | clean | +| 7 | Fitness (Arch-6) | `deno task arch:check` (F-CLI-*) or PENDING_SCRIPT with manual evidence | pass / documented | +| 8 | Merge-readiness (evaluator) | `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` | evaluator/OpenHands pass; NOT a per-slice gate (expensive) | +| 9 | Real push (manual, gated on auth `NEEDS USER`) | `netscript deploy deno-deploy build` (preflight, no push) then interactive push | preflight runs guard; push deferred pending auth confirmation | + +## Risks + +- Platform freshness / auth (mitigated: `NEEDS USER` + re-check + isolated argv wrapper). +- Cross-slice contract drift from unmerged #337/#338 (mitigated: D1/D2 + drift-watch + re-verify). + +## Dependencies + +- **#337** (`feat/deploy-s1-targets-config`) — merged before Implement (Locked D1). +- **#338** (`feat/deploy-s2-doctrine`) — merged/stabilized before Implement (Locked D2). +- External: Deno 2.8 toolchain (native `deno deploy`), a Deno Deploy org + token for real push + (manual verification only). + +## Drift Watch + +- `DeployTargetPort` shape on #338 (operations set, request/result fields) — re-verify at Implement. +- `deploy.targets.*` schema shape on #337 (base fields, `z.ZodType` annotation). +- `deno deploy` CLI flag surface + non-interactive auth mechanism (docs freshness). +- If #338 adds a `deploy` operation or lands the `netscript deploy ` router, revisit + D3/D6. diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/research.md b/.llm/tmp/run/deploy-s6-deno-deploy/research.md new file mode 100644 index 000000000..8667233c7 --- /dev/null +++ b/.llm/tmp/run/deploy-s6-deno-deploy/research.md @@ -0,0 +1,135 @@ +# Research — deploy-s6-deno-deploy + +Slice **#342 [Deploy-S6]** — "Deno Deploy tier-1 adapter (MARQUEE)". Phase 2 of the deployment +epic **#327**, decision **D2**. This is a **PLAN-ONLY** run: research + plan + draft PR, then a +separate-session PLAN-EVAL. No adapter/CLI/schema code is written here. + +## Re-baseline + +- **Carried-in sources:** + - Deployment epic aggregation corpus: + `.claude/worktrees/deployment-research/.llm/tmp/run/epic-deployment-aggregation/` + (`deployment-architecture-spec.md`, `decision-gap-tracker.md`, `sources/deno-deploy/**`). + - **#337** landed contract on branch `feat/deploy-s1-targets-config` + (`packages/config/src/domain/schemas/deploy-schema.ts` + `config-section-types.ts`). + - **#338** target-adapter archetype in-flight on branch `feat/deploy-s2-doctrine` + (`packages/cli/src/kernel/domain/deploy/deploy-target-port.ts`, `deploy-target-registry-port.ts`, + `application/registries/deploy-target-registry.ts`, `domain/deploy/windows-service-deploy-target.ts`, + `extension-points.ts`, doctrine `07-composition-and-extension.md`). +- **Re-derived against `main`** @ `cf1ac47b` (worktree HEAD `feat/deploy-s6-deno-deploy`, forked from + origin/main cf1ac47b on 2026-07-03). +- **What changed vs the carried-in version / key correction:** The epic corpus (written before #338) + sketched a bespoke `DenoDeployAdapterPort { plan/up/down/status/logs/rollback/secrets }` shape and + a `packages/cli/src/public/features/deploy/deno-deploy/` strategy folder mirroring + `build-windows-strategy.ts`. **That is now stale.** #338 has since introduced the *real* + target-adapter archetype as a kernel **port + registry** (`DeployTargetPort` with + `operations: build|install|uninstall`, registered in `DeployTargetRegistry`, exported from + `kernel/extension-points.ts`). The adapter MUST conform to #338's `DeployTargetPort`, **not** the + corpus's speculative 7-verb port. This reconciliation is the central research correction of this + run (see Finding 3 and the operation-mapping open decision in `plan.md`). + +## Findings + +| # | Finding (fact the plan depends on) | How to verify | +| -- | ---------------------------------- | ------------- | +| 1 | **#337 base shape.** `DeployTargetBaseSchema` is a `z.object(deployTargetBaseShape)` raw-shape spread (mode, denoPath, compileTarget, concurrency, bundle*, workspace, v8HeapMb, generateEnvFile, logging, health, docker). `WindowsDeployTargetSchema` extends it by **spread** (`{ ...deployTargetBaseShape, servyCliPath, installBase, servicePrefix }`), and `DeployConfigSchema.targets` is `z.object({ windows: WindowsDeployTargetSchema.optional() }).optional()`. New members are added as sibling keys. | `git -C show feat/deploy-s1-targets-config:packages/config/src/domain/schemas/deploy-schema.ts` | +| 2 | **#337 types.** `config-section-types.ts` declares `DeployTargetBase`, `WindowsDeployTarget extends DeployTargetBase`, `DeployConfig { targets?: { windows?: WindowsDeployTarget } }`. Schemas are annotated `z.ZodType` (so `.extend()` is hidden — spread composition is mandatory). A `deno-deploy` member needs BOTH a `DenoDeployTarget` interface and a `DenoDeployTargetSchema` typed `z.ZodType`. | `git -C show feat/deploy-s1-targets-config:packages/config/src/domain/config-section-types.ts` | +| 3 | **#338 archetype (the port my adapter implements).** `DeployTargetPort { key; label; operations: readonly ('build'\|'install'\|'uninstall')[]; build?/install?/uninstall?: (req: DeployTargetRequest) => Promise }`. `DeployTargetRequest { projectRoot; outputDir? }`. `DeployTargetResult { target; operation; message }`. Registered via `DEFAULT_DEPLOY_TARGETS` in `DeployTargetRegistry` and re-exported from `kernel/extension-points.ts`. | `git -C show feat/deploy-s2-doctrine:packages/cli/src/kernel/domain/deploy/deploy-target-port.ts` | +| 4 | **#338 windows adapter is a thin descriptor.** `WindowsServiceDeployTarget implements DeployTargetPort` with `key='windows-service'`, `label='Windows service'`, `operations=['build','install','uninstall']`; its `build/install/uninstall` currently just return a `DeployTargetResult` "registered" message and **delegate real work to the existing public deploy command pipeline** (`public/features/deploy/**`). The port is an extension-point seam layered over the still-authoritative public commands. | `git -C show feat/deploy-s2-doctrine:packages/cli/src/kernel/domain/deploy/windows-service-deploy-target.ts` | +| 5 | **Doctrine axis.** `07-composition-and-extension.md` names a **Deploy target** axis with variants `windows-service, docker, cloud (future)`, and the required extension shape: `registerDeployTarget('cloud', (ctx) => createCloudDeployFlow(ctx))` — a consumer supplies a *factory* returning a package-owned contract type; **no cross-package inheritance**. Every `Registry` subclass must be exported from `extension-points.ts` or declared internal (audit rule / F-CLI-31). | `git -C show feat/deploy-s2-doctrine:docs/architecture/doctrine/07-composition-and-extension.md` | +| 6 | **Existing public deploy router is windows-hardcoded.** `public/features/deploy/deploy-group.ts` `createDeployCommand(deps)` registers 10 cliffy verbs (build, package-cli, copy, install, start, stop, status, logs, uninstall, upgrade), description "Build and manage NetScript Windows Service deployments", calling `buildWindowsDeployment` directly with no target abstraction. Generalizing to `netscript deploy ` is net-new. | `packages/cli/src/public/features/deploy/deploy-group.ts` | +| 7 | **External-CLI shell pattern.** `ServyCliAdapter implements WindowsServicePort` shells `servy-cli.exe` via a `ProcessPort`, captures stdout/stderr, maps exit code → `{ success, message, code }`. This is the exact pattern to mirror for shelling `deno deploy` (a `ProcessPort`-driven adapter, NOT ad-hoc `Deno.Command`). | `packages/cli/src/public/adapters/servy-cli.ts` | +| 8 | **Config resolver pattern.** `deploy-config-resolvers.ts` `resolveWindowsDeploy(userDeploy?.windows)` reads the target sub-config, applies defaults, returns a `ResolvedWindowsDeployConfig`. A sibling `resolveDenoDeployTarget(userDeploy?.targets?['deno-deploy'])` follows the same shape. NOTE: #337 re-keys targets under `deploy.targets.*`, so the resolver reads `deploy.targets['deno-deploy']`, not `deploy['deno-deploy']`. | `packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts` | +| 9 | **Deno Deploy = NEW platform, source-driven, no Dockerfile.** Corpus + live docs: target is *Deno Deploy* (`console.deno.com`, `deno deploy` CLI), NOT deprecated Deploy Classic / Subhosting (deployctl, sunset 2026-07-20). Serverless microVM; build is remote 5-stage (Queue→Prepare→Install→Build→Deploy) from source; config from `deno.json`/`deno.jsonc` + dashboard. | `sources/deno-deploy/reference-builds.md`; https://docs.deno.com/runtime/reference/cli/deploy/ | +| 10 | **Runtime constraint (critical guard).** Deno Deploy runtime is pinned (corpus: Deno 2.5), runs `--allow-all`, and **rejects custom flags including `--unstable-*`**. NetScript scaffolds routinely use `--unstable-kv`. A deployed app that imports unstable APIs (e.g. Deno KV) will fail on-platform. An **unstable-API preflight guard** is required before push. | `sources/deno-deploy/reference-runtime.md`; decision-gap-tracker.md "Blockers" | +| 11 | **`deno deploy` CLI surface (live, 2026).** `deno deploy` (deploys cwd; `--prod` for production); `deno deploy create --org --app --source github\|local --runtime-mode dynamic\|static --entrypoint --framework-preset --install-command --build-command --static-dir`; `deno deploy env add [--secret]` / `deno deploy env load `; `deno deploy switch --org --app`; `deno deploy logs`; `deno deploy logout`; `deno deploy database`. | https://docs.deno.com/runtime/reference/cli/deploy/ | +| 12 | **Auth model (CI open question).** New `deno deploy` uses token-based auth stored in the OS **keyring**, auto-prompting interactively; supports **user tokens and organization tokens**. The docs page does **not** confirm a non-interactive env var (classic `deployctl` used `DENO_DEPLOY_TOKEN`). Non-interactive CI auth for the new CLI is unconfirmed → surfaced as `NEEDS USER` in the plan. | https://docs.deno.com/runtime/reference/cli/deploy/ ; https://docs.deno.com/deploy/classic/deployctl/ | +| 13 | **Existing debt this touches.** `arch-debt.md` §`cli-deploy-artifacts-missing` records that NetScript has no first-class deploy artifact generation / `netscript deploy` beyond the recorded manual facts. This slice partially advances that debt (adds a real cloud target) but does not close it. | `.llm/harness/debt/arch-debt.md:1300` | +| 14 | **`deno-deploy` naming vs registry key.** #338's windows key is `'windows-service'` (registry key) while the CLI-facing target token and the config member key are `windows`. For this slice the config member + CLI token is `deno-deploy`; the registry key should be a stable identifier (`'deno-deploy'`). Keep config-member key, CLI token, and registry key aligned to `deno-deploy` to avoid a windows-style split. | Finding 3 vs #337 `targets.windows` key | + +## Deno Deploy deployment model (target being wrapped) + +- **Platform:** the NEW **Deno Deploy** (dashboard `console.deno.com`, driven by the built-in + `deno deploy` subcommand). Deploy **Classic** (deployctl / Subhosting v1) is deprecated + (sunset 2026-07-20) and is explicitly a **non-target**. +- **Deployment unit:** an **organization → app** (the app is the deployable; deployments are its + revisions). `deno deploy switch --org --app ` sets the default context; `--org`/`--app` + are otherwise passed per-invocation. +- **Two push modes (a product default decision — see plan open-decision sweep):** + 1. **CLI push** — `deno deploy` from the project root uploads source and triggers a remote build. + Deterministic, one-shot, CI-friendly (modulo the auth question). This is the natural marquee + "one-click" for a CLI tool. + 2. **GitHub-push auto-build** — the Deno Deploy GitHub app builds on push. Zero per-deploy CLI + call, but requires repo wiring and is not a single local command. NetScript would *configure* + it, not *drive* it. +- **Build model:** remote, source-driven, **no Dockerfile**. 5 stages Queue→Prepare→Install→Build→ + Deploy. Build config comes from `deno.json`/`deno.jsonc` (app dir, framework preset, install + command, build command, pre-deploy command, runtime mode dynamic/static) plus dashboard overrides. + NetScript's job is to ensure the scaffolded project's `deno.json` carries valid Deploy build + settings and to invoke/verify the deploy — **wrap, do not reinvent** the remote builder. +- **Runtime constraints:** pinned Deno (2.5 per corpus), `--allow-all`, **no `--unstable-*`**. This + is the single most important product caveat for NetScript, whose KV-backed plugins use + `--unstable-kv`. The adapter must **preflight** for unstable-API usage and refuse/warn. +- **Auth/token:** OS-keyring token, interactive auto-prompt, user or org tokens. Non-interactive CI + token mechanism (env var) unconfirmed for the new CLI (Finding 12) → `NEEDS USER`. +- **Env vars / secrets:** `deno deploy env add [--secret]` and `deno deploy env load `, + scoped per environment (production/preview). Maps naturally onto NetScript's existing + `.env`/`.env.template` generation from the deploy build. +- **Managed data:** Deno Deploy offers managed Postgres/KV (`deno deploy database`). Wiring + NetScript's DB layer to managed PG/KV is **out of scope** for this beta slice (deferred; see + Non-goals) but is noted as a follow-up in the epic. + +## #337 / #338 dependency status + +- **#337 (`feat/deploy-s1-targets-config`)** — landed as a branch, **not yet merged to `main`**. The + `deploy.targets.*` contract this slice extends exists there. **Implementation of #342 is blocked + on #337 merging/rebasing first**; the PLAN proceeds now against the known, read contract + (Findings 1–2). Recorded as a hard dependency in `plan.md` (Locked D1). +- **#338 (`feat/deploy-s2-doctrine`)** — in-flight branch, **not merged**. It introduces the + `DeployTargetPort` + `DeployTargetRegistry` + `extension-points.ts` archetype this adapter + conforms to (Findings 3–5) and the doctrine axis text. **Implementation of #342 is blocked on #338 + merging first** (or at minimum on its port/registry surface stabilizing). PLAN designs against the + read port; PLAN-EVAL will check conformance. Recorded as Locked D2. +- **Merge ordering for the Implement phase:** #337 → #338 → rebase #342. Stated explicitly so the + Implement phase does not begin against a missing contract or a shifting port. + +## jsr-audit surface scan (package/plugin waves) + +- **Surface scanned (planned):** `packages/config` public schema exports (new `DenoDeployTarget` + type + `DenoDeployTargetSchema`) and `packages/cli` `mod.ts` / `extension-points.ts` (new registry + member; no new public *type* export expected — the adapter is internal, registered via the + existing `DeployTargetRegistry`). +- **Slow-type / surface risks:** `packages/config` runs under `isolatedDeclarations`; the schema is + annotated `z.ZodType` exactly like `WindowsDeployTargetSchema` to keep fast + types. The new `DenoDeployTarget` interface must carry explicit field types (no inferred). The CLI + adapter adds no new slow-type public surface (it implements an existing port). `packages/config` + is a **published** package → `deno publish --dry-run` is a required gate for the schema slice. +- **Verdict:** low surface risk if the #337 spread-composition + `z.ZodType` annotation pattern is + followed exactly; PLAN-EVAL should confirm the annotation before slicing. + +## Non-goals (deferred to `stable` per D-tiers; explicit) + +- **HA / multi-region / autoscaling policy** — Deno Deploy manages this; NetScript does not model it + in beta. +- **Managed Postgres/KV wiring** (`deno deploy database` → NetScript DB layer) — deferred follow-up. +- **GitHub-push auto-build orchestration** beyond, at most, emitting/validating the build settings — + CLI-push is the beta default (pending the open decision). Full GitHub-app provisioning is deferred. +- **Rollback / promote / traffic-splitting verbs** — the corpus sketched `rollback`; #338's port + exposes only `build|install|uninstall`. Beta conforms to the port; richer verbs are a follow-up + that must be coordinated with the #338 port owner (see plan open decision). +- **Deploy Classic / deployctl / Subhosting** — deprecated platform, explicitly not targeted. +- **Generalizing every existing windows verb** (start/stop/upgrade/copy/package-cli) to deno-deploy — + those are windows-service lifecycle concepts with no cloud analogue; beta implements only the + operations the `DeployTargetPort` defines. + +## Open questions (closed in `plan.md`) + +1. How do the port's `build|install|uninstall` operations map onto Deno Deploy semantics + (build=preflight/settings, install=push-deploy, uninstall=delete-app)? → plan Open-Decision + a + possible coordinated port-vocabulary note with #338. +2. CLI-push vs GitHub-push default for the beta marquee? → `NEEDS USER`. +3. Non-interactive CI auth mechanism for the new `deno deploy` (env var / org token)? → + `NEEDS USER` + freshness re-check at Implement time. +4. Does the router generalization to `netscript deploy ` land in this slice or is the + deno-deploy target reached only through the registry until #338's router work lands? → plan Locked + decision (register-first, minimal CLI reach). From 10812458c76673f3ba62bf3db4ebf985d4f2120f Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 10:13:43 +0200 Subject: [PATCH 02/14] docs(deploy): [Deploy-S6] revise research+plan to Archetype-7 7-op contract (#342) Rev 2 after peer FAIL_PLAN (B1): select Archetype 7 (composite Arch2+Arch6), conform to the canonical 7-op deploy contract, record that the 3-op->7-op port expansion is owned by #339/#340 (this slice CONSUMES it, does not redefine it), re-baseline against merged main (#352 config + 3137e455 seed port already landed), add the 7-op->Deno-Deploy op-mapping, and correct the dispatch lane to Opus 4.8 sub-agents. Still planning-only; awaits PLAN-EVAL. Refs #342 #327 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012wKHquACkXnWPDgJYhhFjN --- .llm/tmp/run/deploy-s6-deno-deploy/plan.md | 309 +++++++++++------- .../tmp/run/deploy-s6-deno-deploy/research.md | 235 ++++++------- 2 files changed, 311 insertions(+), 233 deletions(-) diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/plan.md b/.llm/tmp/run/deploy-s6-deno-deploy/plan.md index 1ecd125bf..e0f8a13ff 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/plan.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/plan.md @@ -1,131 +1,187 @@ # Plan: Deno Deploy tier-1 adapter (#342 [Deploy-S6], MARQUEE) +> **Revision 2 (post-FAIL_PLAN).** Rev 1 was returned `FAIL_PLAN` (blocking finding B1): it +> misattributed the shipped 3-op `DeployTargetPort` to #338, demoted the ratified **7-op Archetype 7** +> contract to "stale corpus," and premised "#337 not merged." This revision: (a) selects +> **Archetype 7 (composite)** as primary; (b) conforms to the canonical **7-op** deploy contract; +> (c) records that the 3-op→7-op **port expansion is OWNED by #339/#340** and #342 **consumes** it +> (does not redefine it); (d) re-baselines against **merged `origin/main`** (#352 config + +> `3137e455` seed port already landed); (e) corrects the dispatch lane to **Opus 4.8 sub-agents**. +> Still **PLAN-ONLY** — no adapter/CLI/schema code is written; Implement waits for PLAN-EVAL. + ## Run Metadata | Field | Value | | -------------- | --------------------------------------------------------------- | | Run ID | `deploy-s6-deno-deploy` | -| Branch | `feat/deploy-s6-deno-deploy` (off origin/main `cf1ac47b`) | -| Phase | `plan` (PLAN-ONLY; Implement blocked on #337 + #338 + PLAN-EVAL) | +| Branch | `feat/deploy-s6-deno-deploy` (forked off `cf1ac47b`; re-baselined against merged `origin/main` @ `37108172`) | +| Phase | `plan` (PLAN-ONLY; Implement blocked on merge order below + PLAN-EVAL PASS) | | Target | `packages/cli` (adapter + wiring) + `packages/config` (schema) | -| Archetype | `6 - CLI/Tooling` (primary); folds `2 - Integration`; config member follows `1 - Small-Contract` | +| Archetype | **7 — Deploy Target Adapter (composite: Arch 2 Integration + Arch 6 CLI/Tooling)** | | Scope overlays | `none` (no Fresh/UI; no new long-running service; Aspire-optional at runtime gate) | ## Archetype -**Primary: ARCHETYPE-6 (CLI / Tooling).** The deliverable ships a user-run command flow in the -`@netscript/cli` package (`netscript deploy deno-deploy `). Per the archetype decision order, -when a package "ships a binary or command-line flow" it is Arch-6, and smaller concerns fold inside. - -**Folded: ARCHETYPE-2 (Integration).** The substance is wrapping an external system — the Deno -Deploy platform via the `deno deploy` CLI — behind a package-owned port. #338 already provides that -port (`DeployTargetPort`) and registry (`DeployTargetRegistry`); the deno-deploy adapter is a new -**adapter on an existing axis**, exactly the Arch-2 "named adapter behind an owned port" shape. It -must shell the external CLI through a `ProcessPort` (mirroring `ServyCliAdapter`), never ad-hoc -`Deno.*` in presentation/application layers (F-CLI-5/16, AP-11). - -**Config member: ARCHETYPE-1 (Small-Contract) sub-concern.** The `deploy.targets['deno-deploy']` -schema member is a types+schema addition to the published `@netscript/config` package, following the -exact spread-composition + `z.ZodType` pattern #337 established for `windows` (Findings 1–2). - -Justification for not splitting: doctrine says "if two archetypes apply, choose the larger and fold -the smaller." The CLI flow is the marquee; integration and the small contract are folded. The config -schema lives in a *different published package* (`@netscript/config`), so its slice carries the -Arch-1 publishability gates (F-6 / publish dry-run) while the CLI slices carry the F-CLI-* family. +**Primary: ARCHETYPE-7 — Deployment Target Adapter (composite).** Ratified by **#338** (impl #357, +IMPL-EVAL PASS); doctrine at `.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md`. It is a +**composite** that composes two archetypes and folds **neither**: + +- **Composed Archetype 2 (Integration — port/adapter core).** The substance is wrapping an external + system — the Deno Deploy platform via the `deno deploy` CLI — behind a **package-owned port** + (the 7-op deploy port) registered in a **closed-on-key** `deploy-target-registry`. The deno-deploy + adapter is a new **adapter on that existing port axis**: it shells the external CLI through a + `ProcessPort` (mirroring `ServyCliAdapter`), never ad-hoc `Deno.*` in features/presentation + (F-CLI-16, AP-11). +- **Composed Archetype 6 (CLI/Tooling — thin router).** The user reaches the target through a **thin** + `netscript deploy ` router that carries **no target-specific business logic** + (R-DEPLOY-2) — it dispatches to the registry. + +**Config member (small-contract sub-concern).** The `deploy.targets['deno-deploy']` schema member is +a types+schema addition to the published `@netscript/config` package, following the **exact +spread-composition + `z.ZodType` pattern** merged for `windows` via **#352** (Findings 1–2). Per +R-DEPLOY-4 the member **spreads** `deployTargetBaseShape` — no per-target base-class hierarchy. + +**Why Archetype 7 and not "Arch-6 folding Arch-2":** #338 established that deployment targets are a +first-class composite archetype with its own rule family (R-DEPLOY-1..5) and gates (F-DEPLOY-1/2). +Rev 1's "Arch-6 primary folding Arch-2" framing is superseded; this slice is governed by Archetype 7 +and must satisfy its rules, not just the generic CLI family. + +## Canonical contract (7-op) — the correction + +The deploy port exposes the **uniform 7-op contract** (Archetype 7): **`plan`/`emit` · `up` · `down` +· `status` · `logs` · `rollback` · `secrets`**. Each adapter implements the **subset it supports** +(R-DEPLOY-1). The 3-op `build|install|uninstall` port currently on `main` is a **seed stub from +`3137e455`** (an unrelated command-registry slice), scheduled for expansion to 7-op by **#339/#340**. + +**#342 CONSUMES the expanded 7-op port; it does NOT define or redefine it** (research Finding 6; +`port-ownership.md`). The deno-deploy adapter implements this subset: + +| Canonical op | Deno Deploy mapping | Support | +| -------------- | ------------------- | ------- | +| `plan`/`emit` | Preflight: validate `deno.json` Deploy build settings + run the **unstable-API guard**; compute the plan, no platform mutation. | Supported (local) | +| `up` | **`deno deploy`** (preview) / **`deno deploy --prod`** (production) — the marquee one-click push. | Supported (marquee) | +| `down` | Delete the Deno Deploy app/deployment (platform-native; CLI verb TBD). | Supported (platform-native) | +| `status` | Deployment/app state via platform API. | Supported | +| `logs` | **`deno deploy logs`**. | Supported | +| `rollback` | Platform-native rollback (repoint to prior good deployment), routed via the **core rollback convention** (R-DEPLOY-3) — not a no-op. | Supported | +| `secrets` | **`deno deploy env add [--secret]` / `env load`**, via the **core secrets convention** (R-DEPLOY-3). | Supported | +| (host install/uninstall) | N/A — hosted platform has no OS-service install. | **Explicitly unsupported** (declared subset) | + +Legacy CLI verbs (`build/install/uninstall`) remain **user-facing aliases** onto canonical ops per +the #339/#340 verb-lock (`build`→`plan`, `install`→`up`, `uninstall`→`down`); the adapter's declared +`operations` set uses the **canonical** names. ## Current Doctrine Verdict -N/A as a fresh verdict for this consumer-of-doctrine slice. The governing doctrine is -`07-composition-and-extension.md` (Deploy target axis; `registerDeployTarget` factory pattern; no -cross-package inheritance; every `Registry` in `extension-points.ts`) as amended on -`feat/deploy-s2-doctrine` (#338). The adapter conforms to that axis; it does not change doctrine. +N/A as a fresh verdict for this consumer-of-doctrine slice. Governing doctrine: **Archetype 7** +(#338/#357) + `07-composition-and-extension.md` (Deploy target axis; `registerDeployTarget` factory +pattern; no cross-package inheritance; every `Registry` in `extension-points.ts`). The adapter +conforms to that axis; it does not change doctrine. **R-DEPLOY-3** requires the shared conventions +(health, OTEL, **secrets, rollback**) to live in the deploy **core**, not in this adapter. ## Axioms in Play | Axiom | Why it matters | | ----- | -------------- | | A2 (public surface minimal) | Adapter is internal; only the config type + schema is new public surface. | -| A5 / A6 (ports & adapters, no cross-boundary inheritance) | Adapter implements `DeployTargetPort`; consumer supplies a factory, not a subclass (doctrine R-COMP). | +| A5 / A6 (ports & adapters, no cross-boundary inheritance) | Adapter implements the port; consumer supplies a factory, not a subclass (R-COMP, R-DEPLOY-4). | | A11 (edge-only side effects) | `deno deploy` shelling goes through a `ProcessPort` in `adapters/`; no `Deno.Command` in features/presentation (F-CLI-16, AP-11). | -| A8 (extension via typed registry) | New target registered in `DeployTargetRegistry`, exported from `extension-points.ts` (F-CLI-31, AP-24). | +| A8 (extension via typed registry) | New target registered in the closed-on-key `deploy-target-registry`, exported from `extension-points.ts` (F-CLI-31, AP-24). | | A13/A14 (declarative composition, documented permissions) | Composition declarative (F-CLI-27); README declares `--allow-run` for `deno deploy` + network (AP-19). | ## Goal Add a tier-1 **Deno Deploy** deployment target to NetScript: a `deploy.targets['deno-deploy']` -config member (extends #337's base) + a resolver + a `DenoDeployTarget` adapter implementing #338's -`DeployTargetPort`, which shells the native `deno deploy` CLI (source build, no Dockerfile) to push a -scaffolded NetScript project to the new Deno Deploy platform — reachable via the CLI as -`netscript deploy deno-deploy `. Includes the **unstable-API preflight guard** (Finding 10). +config member (spreads #352's `deployTargetBaseShape`) + a resolver + a `DenoDeployTarget` adapter +implementing the **subset of the 7-op deploy port** (consumed from #339/#340) that Deno Deploy +supports, shelling the native `deno deploy` CLI (source build, no Dockerfile) to push a scaffolded +NetScript project to the new Deno Deploy platform — reachable via the thin router as +`netscript deploy deno-deploy `. Includes the **unstable-API preflight guard** (Finding 9) in +`plan`, and routes `secrets`/`rollback` through the **core conventions** (R-DEPLOY-3). ## Scope - `packages/config`: `DenoDeployTarget` interface + `DenoDeployTargetSchema` (`z.ZodType<…>`, spread-composed on `deployTargetBaseShape`); add `deno-deploy` key to `DeployConfigSchema.targets` and `DeployConfig.targets`. -- `packages/cli` kernel: `DenoDeployTarget` adapter implementing `DeployTargetPort` - (`key='deno-deploy'`), registered in `DEFAULT_DEPLOY_TARGETS`; a `deno deploy` process wrapper - adapter (mirrors `ServyCliAdapter`); an unstable-API preflight checker; a - `resolveDenoDeployTarget` config resolver. -- `packages/cli` CLI reach: expose the deno-deploy target's operations through the deploy command - surface (minimal, register-first; see Locked D6), with `--org`/`--app`/`--prod`/`--env-file` and - `--dry-run` options mapped onto `deno deploy` flags. +- `packages/cli` kernel: `DenoDeployTarget` adapter implementing the **7-op port** subset + (`key='deno-deploy'`, `operations = ['plan','up','down','status','logs','rollback','secrets']`); a + `deno deploy` process-wrapper adapter (mirrors `ServyCliAdapter`); an unstable-API preflight + checker (used by `plan`); a `resolveDenoDeployTarget` config resolver. +- `packages/cli` router reach: register the target so the thin `netscript deploy deno-deploy ` + router dispatches its ops (R-DEPLOY-2 — no target logic in the router), with `--org`/`--app`/ + `--prod`/`--env-file`/`--dry-run` mapped onto `deno deploy` flags. +- **Core conventions (consume, do not fork):** `secrets` and `rollback` route through the deploy + **core** convention primitives (R-DEPLOY-3) owned by #339/#340's core, not re-implemented here. - Tests: schema round-trip/defaults; resolver defaults; adapter unit tests with a fake `ProcessPort` - (asserting the exact `deno deploy` argv, exit-code mapping, and unstable-guard refusal); registry - membership test. + (asserting exact `deno deploy` argv, exit-code mapping, unstable-guard refusal, declared-subset + reporting for unsupported ops); registry membership test. - Docs: README permissions block (`--allow-run`, network) + a short how-to note; drift/worklog. ## Non-Scope -- Merging #337/#338 (external prerequisites; this slice rebases onto them). -- Managed Postgres/KV wiring (`deno deploy database`), HA/multi-region, rollback/promote/traffic - verbs, GitHub-push auto-build provisioning, Deploy Classic/deployctl — all deferred (research - Non-goals). -- Generalizing windows-only lifecycle verbs (start/stop/upgrade/copy/package-cli) to the cloud target - — no cloud analogue; beta implements only `DeployTargetPort` operations. -- Full `netscript deploy ` router refactor of the windows-hardcoded `deploy-group.ts` - — that generalization is #338/epic router work; this slice reaches the target through the registry - with the smallest necessary CLI surface (Locked D6). +- **Owning or redefining the deploy port / op vocabulary** — owned by **#339/#340**; #342 consumes + the expanded 7-op port and rebases onto its port-contract commit. +- Building the `OsServicePort` + the full multi-target router — #339/#340 core work; #342 adds the + deno-deploy adapter behind it + its registry entry. +- Merging #357 / #339 / #340 (external prerequisites; this slice rebases onto them — merge order + below). +- Managed Postgres/KV wiring (`deno deploy database`), HA/multi-region, GitHub-push auto-build + provisioning, Deploy Classic/deployctl — deferred (research Non-goals). +- Authoring the shared `secrets`/`rollback`/health/OTEL **core conventions** — those are R-DEPLOY-3 + core primitives (#339/#340); #342 consumes them for the deno-deploy mapping. ## Hidden Scope -- **Operation-vocabulary mismatch (Finding 3 vs corpus).** #338's port exposes only - `build|install|uninstall`; Deno Deploy's natural verb is "deploy/push". The plan maps semantics - onto the existing 3 operations (Locked D3) rather than editing #338's port, to avoid cross-slice - coupling. If the port owner adds a `deploy` operation, revisit. +- **Port timing (Finding 3/6).** `main` currently carries only the 3-op seed stub. #342 Implement + **must not** begin against the 3-op stub; it rebases onto #339/#340's expanded 7-op port commit + first (merge order below). Designing the adapter against the 7-op contract now is safe because the + contract is ratified doctrine (#338). - **`isolatedDeclarations` on config** — the new interface needs explicit field types; the schema - must keep the `z.ZodType` annotation or fast-types/`deno doc` break (Finding 2). + must keep the `z.ZodType` annotation or fast-types/`deno doc`/JSR slow-types break (Finding 2). - **Unstable-API guard is not optional** — without it the marquee "one-click" silently ships an app - that dies on-platform (KV `--unstable-kv`). This is a first-class slice, not a nice-to-have. + that dies on-platform (KV `--unstable-kv`). First-class slice, not a nice-to-have (Finding 9). +- **`secrets`/`rollback` must not be re-implemented per-target** — R-DEPLOY-3 requires the core + convention; the adapter only maps the convention onto `deno deploy env`/platform rollback. - **Windows key precedent** — keep config member key, CLI token, and registry key all `deno-deploy` - (Finding 14) to avoid the windows `windows` vs `windows-service` split. + (Finding 6 / windows precedent) to avoid the `windows` vs `windows-service` split. ## Locked Decisions | ID | Decision | Rationale | | -- | -------- | --------- | -| D1 | **Implement phase depends on #337 merged/rebased first.** Plan proceeds now against the read contract; no config code committed until the `deploy.targets.*` base exists on the working base. | #337 owns `DeployTargetBaseSchema`/`DeployConfigSchema` this member extends (Findings 1–2). | -| D2 | **Adapter conforms to #338's `DeployTargetPort`/`DeployTargetRegistry`; Implement depends on #338 merged/stabilized.** Design against the read port now. | #338 owns the target-adapter archetype (Findings 3–5); PLAN-EVAL checks conformance. | -| D3 | **Operation mapping:** `build` = preflight (validate `deno.json` Deploy build settings + run the unstable-API guard, no remote call); `install` = **push/deploy** via `deno deploy` (`--prod` gated by an option); `uninstall` = delete the app/deployment. Conform to the existing 3-op port; do **not** edit #338's port in this slice. | Mirrors windows (`install`=create, `uninstall`=remove) and avoids cross-slice port coupling. Marquee "deploy" is surfaced as a CLI verb aliasing `install` (see D6). | -| D4 | **CLI push is the beta driver** (`deno deploy` from project root), not GitHub-push auto-build. GitHub-push is deferred. | Shortest deterministic one-click for a CLI tool; matches D2 "shortest path" marquee intent. *(Product-facing default — also flagged in Open-Decision sweep for user confirmation.)* | -| D5 | **Shell `deno deploy` through a `ProcessPort` adapter** (new `DenoDeployCliAdapter`), mirroring `ServyCliAdapter`; capture stdout/stderr, map exit code → `DeployTargetResult`. No `Deno.Command` outside `kernel/adapters/**`. | Arch-6 R-A6-N8 / F-CLI-16; Arch-2 named-adapter-behind-port; testable with a fake port. | -| D6 | **Register-first, minimal CLI reach.** Add the target to `DeployTargetRegistry` (authoritative) and expose it through the deploy command surface with the smallest change that lets `netscript deploy deno-deploy ` run; do **not** refactor the windows-hardcoded `deploy-group.ts` router. | Keeps the slice bounded; the full `deploy ` router is epic/#338 work (research Finding 6). | -| D7 | **Registry key = CLI token = config member key = `deno-deploy`.** | Avoid the windows `windows`/`windows-service` split (Finding 14). | -| D8 | **Unstable-API preflight guard is a required slice.** `build`/`install` scan the entrypoint + `deno.json` imports for `--unstable-*`-requiring APIs (KV et al.) and refuse (or hard-warn) before push. | Deno Deploy rejects `--unstable-*`; NetScript KV plugins use `--unstable-kv` (Finding 10). | +| D1 | **Config base is MERGED (#352).** Implement adds the `deno-deploy` member by **spreading** `deployTargetBaseShape` (R-DEPLOY-4). No "rebase onto unmerged #337" gate — that rev-1 premise is dropped. | #352 (`44d7945b`) landed `DeployTargetBaseSchema`/`DeployConfigSchema` on `main` (Findings 1–2). | +| D2 | **Conform to Archetype 7 (#338/#357); cite it by name.** Adapter satisfies R-DEPLOY-1..5 + F-DEPLOY-1/2. | #338 ratified the composite archetype + 7-op contract (Findings 4–5); PLAN-EVAL checks conformance. | +| D3 | **CONSUME the 7-op port from #339/#340 — do NOT redefine it.** The adapter's `operations` uses the **canonical** op names; `build/install/uninstall` are user-facing aliases only. Implement rebases onto #339/#340's expanded-port commit. | Port expansion is **owned** by #339/#340 (Finding 6; `port-ownership.md`). Redefining it = the B1-class error. | +| D4 | **Op mapping = the 7-op→Deno-Deploy table above.** Deno Deploy implements the declared subset; host install/uninstall is explicitly unsupported. `secrets`/`rollback` route via the **core conventions** (R-DEPLOY-3), not adapter-local logic. | Uniform contract + R-DEPLOY-1 (subset) + R-DEPLOY-3 (conventions in core). | +| D5 | **CLI push is the beta driver** (`deno deploy` from project root → `up`), not GitHub-push auto-build. GitHub-push deferred. | Shortest deterministic one-click marquee. *(Product-facing default — flagged NEEDS USER for confirmation; reversible.)* | +| D6 | **Shell `deno deploy` through a `ProcessPort` adapter** (`DenoDeployCliAdapter`), mirroring `ServyCliAdapter`; capture stdout/stderr, map exit code → deploy result. No `Deno.Command` outside `kernel/adapters/**`. | Arch-2 named-adapter-behind-port + F-CLI-16; testable with a fake port. | +| D7 | **Thin router, register-first (R-DEPLOY-2).** Register the target in the closed-on-key `deploy-target-registry`; the `netscript deploy deno-deploy ` router only dispatches — **no target-specific logic in the command surface**. The generalized router is #339/#340 work; #342 supplies the adapter + registry entry. | R-DEPLOY-2; keeps the slice bounded. | +| D8 | **Registry key = CLI token = config member key = `deno-deploy`.** | Avoid the `windows`/`windows-service` split (Finding 6). | +| D9 | **Unstable-API preflight guard is a required slice**, invoked by `plan`. Scans entrypoint + `deno.json` imports for `--unstable-*`-requiring APIs (KV et al.) and refuses (or hard-warns) before `up`. | Deno Deploy rejects `--unstable-*`; NetScript KV plugins use `--unstable-kv` (Finding 9). | +| D10 | **Dispatch lane = Opus 4.8 sub-agents (this epic).** Implementers = Opus 4.8 sub-agents; evaluators = separate Opus session (or Codex GPT-5.5). **WSL Codex / OpenHands is NOT the impl lane for the deployment epic.** | Epic dispatch-lane override (Finding 14; `port-ownership.md`). | + +## Merge order (Implement gate) + +``` +#357 (Archetype-7 doctrine) → #339/#340 expanded 7-op port commit → rebase #342 onto both (+ current main, which already carries #352 + 3137e455) +``` + +Implement does NOT start until #339/#340's port-contract commit exists and #342 is rebased onto it. +Designing now against the ratified 7-op contract is safe and unblocks PLAN-EVAL. ## Open-Decision Sweep | Decision | Status | Notes | | -------- | ------ | ----- | -| Op-vocabulary: reuse `build/install/uninstall` vs add a `deploy` op to #338's port | **safe to defer** (resolved as D3 for beta) | Deferring does not force rework: D3 conforms to the current port; adding a `deploy` op later is additive and coordinated with #338's owner. | -| **CLI-push vs GitHub-push default** | **NEEDS USER** (proceeding on D4 = CLI-push) | Product-facing default for the marquee. D4 is a reversible assumption; if user wants GitHub-push-first, the driver changes but the port/schema/guard do not. | -| **Non-interactive CI auth for new `deno deploy`** (env var / org token) | **NEEDS USER** + freshness re-check at Implement | Docs confirm keyring + interactive prompt + user/org tokens but not a documented non-interactive env var (Findings 11–12). Blocks CI/e2e automation of a *real* push, not the adapter/unit design. Beta may ship CLI-push with an interactive-auth caveat + a `--token`/env passthrough pending confirmation. | -| Unstable-guard = **refuse** vs **warn-and-continue** | safe to defer (default: refuse for `--prod`, warn for preview) | Reversible flag on the guard; does not affect its structure. | -| Whether beta exposes `env`/`logs` passthrough verbs | safe to defer | Additive CLI verbs; out of the D6 minimal reach unless trivial. | +| **CLI-push vs GitHub-push default** | **NEEDS USER** (proceeding on D5 = CLI-push) | Product-facing marquee default. Reversible: if GitHub-push-first is wanted, the `up` driver changes but the port/schema/guard/conventions do not. | +| **Non-interactive CI auth for new `deno deploy`** (env var / org token) | **NEEDS USER** + freshness re-check at Implement | Docs confirm keyring + interactive prompt + user/org tokens but not a documented non-interactive env var (Finding 11). Blocks CI/e2e automation of a *real* push, not the adapter/unit design. | +| Unstable-guard = **refuse** vs **warn-and-continue** | safe to defer (default: refuse on `--prod`, warn on preview) | Reversible flag on the guard; does not affect its structure. | +| Exact `down`/`status`/`rollback` CLI-vs-API surface | safe to defer (confirm at Implement vs then-current platform API) | Each op isolated behind the port; platform-native mechanism confirmed at Implement freshness re-check. | -> No open decision would force rework if deferred: the two `NEEDS USER` items affect the *driver -> default* and *CI auth wiring*, not the port conformance, schema shape, adapter structure, or guard — -> which are all locked. Recorded so PLAN-EVAL can confirm the deferral is safe. +> No open decision forces rework if deferred: the two `NEEDS USER` items affect the *driver default* +> and *CI auth wiring*, not port conformance, schema shape, adapter structure, the guard, or the +> core-convention routing — all locked. Recorded so PLAN-EVAL can confirm the deferral is safe. ## Change Map @@ -134,77 +190,85 @@ scaffolded NetScript project to the new Deno Deploy platform — reachable via t | Config type | `packages/config/src/domain/config-section-types.ts` | Add `DenoDeployTarget extends DeployTargetBase`; add `deno-deploy?` to `DeployConfig.targets`. | S1 | | Config schema | `packages/config/src/domain/schemas/deploy-schema.ts` | Add `DenoDeployTargetSchema: z.ZodType` (spread `deployTargetBaseShape` + Deno-Deploy fields); add `'deno-deploy'` key to `DeployConfigSchema.targets`. | S1 | | Config test | `packages/config/**/deploy-schema*_test.ts` (co-located) | Round-trip + defaults + optionality for the new member. | S1 | -| Adapter port impl | `packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts` | `class DenoDeployTarget implements DeployTargetPort` (`key='deno-deploy'`, ops `['build','install','uninstall']`, D3 mapping). | S3 | -| Process wrapper | `packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts` | `DenoDeployCliAdapter` shelling `deno deploy` via `ProcessPort` (mirrors `ServyCliAdapter`); argv builders for create/deploy/env/uninstall. | S3 | | Unstable guard | `packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts` (or `adapters/…` if it reads FS) | Scan entrypoint + `deno.json` for `--unstable-*`-requiring APIs; return a violation report. | S2 | -| Registry wiring | `packages/cli/src/kernel/application/registries/deploy-target-registry.ts` | Add `DENO_DEPLOY_TARGET` to `DEFAULT_DEPLOY_TARGETS`. | S3 | -| Extension-points | `packages/cli/src/kernel/extension-points.ts` | No new export needed (registry already exported); confirm re-export completeness (F-CLI-31). | S3 | +| Adapter (7-op subset) | `packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts` | `class DenoDeployTarget implements <7-op DeployPort>` (`key='deno-deploy'`, `operations = ['plan','up','down','status','logs','rollback','secrets']`, D4 mapping; `secrets`/`rollback` delegate to core conventions). | S3 | +| Process wrapper | `packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts` | `DenoDeployCliAdapter` shelling `deno deploy` via `ProcessPort` (mirrors `ServyCliAdapter`); argv builders for deploy/env/logs/create. | S3 | +| Registry wiring | `packages/cli/src/kernel/application/registries/deploy-target-registry.ts` | Add `DENO_DEPLOY_TARGET` to `DEFAULT_DEPLOY_TARGETS` (closed-on-key registry). | S3 | +| Extension-points | `packages/cli/src/kernel/extension-points.ts` | Confirm registry re-export completeness (F-CLI-31); no new export expected. | S3 | | Config resolver | `packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts` | Add `resolveDenoDeployTarget(userDeploy?.targets?['deno-deploy'])` → `ResolvedDenoDeployConfig` + defaults. | S4 | -| CLI reach | `packages/cli/src/public/features/deploy/**` (minimal, D6) | Surface `deno-deploy` operations + `--org/--app/--prod/--env-file/--dry-run` options mapped to `deno deploy` flags. | S4 | -| Adapter tests | co-located `*_test.ts` under the new folders | Fake `ProcessPort`: assert exact argv, exit-code→result, guard refusal, registry membership. | S2–S4 | -| README/permissions | `packages/cli/README.md` (permissions block) | Declare `--allow-run` (deno deploy) + network; note unstable-API caveat + auth caveat. | S4 | +| Router reach | `packages/cli/src/public/features/deploy/**` (thin, R-DEPLOY-2) | Dispatch `deno-deploy` ops through the router + `--org/--app/--prod/--env-file/--dry-run` → `deno deploy` flags; **no target logic in the surface**. | S4 | +| Adapter tests | co-located `*_test.ts` under the new folders | Fake `ProcessPort`: exact argv, exit-code→result, guard refusal, declared-subset for unsupported ops, registry membership. | S2–S4 | +| README/permissions | `packages/cli/README.md` (permissions block) | Declare `--allow-run` (deno deploy) + network; note unstable-API + auth caveats. | S4 | | Debt note | `.llm/harness/debt/arch-debt.md` §`cli-deploy-artifacts-missing` | Update: cloud target added; debt advanced, not closed. | S4 | ## Commit Slices -Ordered per Arch-6 "kernel domain + ports + abstracts first; registries + adapters next; features -one at a time; composition last." Each is independently committed, pushed, PR-commented. All slices -are gated behind D1/D2 (rebase onto merged #337 + #338) at Implement time. +Ordered per Archetype 7: kernel port-conformant domain + guard first; registry + adapters next; +router reach + resolver + docs last. Each is independently committed, pushed, PR-commented. All +slices are gated behind the **merge order** (rebase onto #357 + #339/#340 port commit) at Implement. | # | Slice | Proves | Gate | Files | | - | ----- | ------ | ---- | ----- | -| S1 | **Config member** — `DenoDeployTarget` type + `DenoDeployTargetSchema` + `targets['deno-deploy']`. | The published config surface accepts a valid deno-deploy target and rejects bad shapes; fast-types intact. | `deno task check` (config, `--unstable-kv` where needed) + config `_test.ts` + `deno publish --dry-run` (config) + `run-deno-check.ts --root packages/config --ext ts`. | config type + schema + test. | -| S2 | **Unstable-API guard** (pure/kernel) + tests. | Guard flags `--unstable-*`-requiring imports (KV) in a fixture project and passes a clean one. | `run-deno-check.ts --root packages/cli` + guard `_test.ts` (fixtures) + fitness F-CLI-1/16 manual. | `unstable-api-guard.ts` + test. | -| S3 | **Adapter + process wrapper + registry** — `DenoDeployTarget implements DeployTargetPort`, `DenoDeployCliAdapter` (ProcessPort), `DEFAULT_DEPLOY_TARGETS` entry. | Adapter emits the exact `deno deploy` argv, maps exit codes, refuses on guard violation; registry lists `deno-deploy`. | adapter/registry `_test.ts` with fake `ProcessPort` + `run-deno-check.ts` + fitness F-CLI-16/17/19/28/31 manual + F-4 (kernel≠surface imports). | adapter, cli-wrapper, registry, extension-points confirm. | -| S4 | **Resolver + CLI reach + docs/debt** — `resolveDenoDeployTarget`, minimal deploy-surface reach, README permissions, arch-debt update. | `netscript deploy deno-deploy build` runs the guard/preflight against a scaffold without a real push; options map to `deno deploy` flags; README declares perms. | resolver `_test.ts` + CLI help/name check + `deno task lint` + `run-deno-fmt.ts --root packages/cli --ext ts` + fitness F-CLI-26/27 + AP-19 manual. | resolver, deploy-surface reach, README, arch-debt. | +| S1 | **Config member** — `DenoDeployTarget` type + `DenoDeployTargetSchema` + `targets['deno-deploy']`. | Published config surface accepts a valid deno-deploy target and rejects bad shapes; fast-types intact; spreads base (R-DEPLOY-4). | `run-deno-check.ts --root packages/config --ext ts` + config `_test.ts` + `deno publish --dry-run` (config). | config type + schema + test. | +| S2 | **Unstable-API guard** (pure/kernel) + tests. | Guard flags `--unstable-*`-requiring imports (KV) in a fixture project and passes a clean one; consumed by `plan`. | `run-deno-check.ts --root packages/cli` + guard `_test.ts` (fixtures) + F-CLI-1/16 manual. | `unstable-api-guard.ts` + test. | +| S3 | **Adapter (7-op subset) + process wrapper + registry** — `DenoDeployTarget` implements the 7-op port subset; `DenoDeployCliAdapter` (ProcessPort); `DEFAULT_DEPLOY_TARGETS` entry. | Adapter declares the canonical op subset, emits exact `deno deploy` argv, maps exit codes, refuses on guard violation, delegates `secrets`/`rollback` to core conventions; registry lists `deno-deploy`. | adapter/registry `_test.ts` (fake `ProcessPort`) + `run-deno-check.ts` + F-DEPLOY-1 + F-CLI-16/17/19/28/31 + F-4 manual. | adapter, cli-wrapper, registry, extension-points confirm. | +| S4 | **Resolver + thin router reach + docs/debt** — `resolveDenoDeployTarget`, thin router dispatch, README permissions, arch-debt update. | `netscript deploy deno-deploy plan` runs the guard/preflight against a scaffold without a real push; router carries no target logic (R-DEPLOY-2); options map to `deno deploy` flags; README declares perms. | resolver `_test.ts` + CLI help/name check + `run-deno-lint.ts` + `run-deno-fmt.ts` + F-DEPLOY-2 + F-CLI-26/27 + AP-19 manual. | resolver, deploy-surface reach, README, arch-debt. | -Slice count: **4** (< 30). If S3 reveals the #338 router must change to reach the target at all, -that is a **rescope trigger** (Arch-6 "command vocabulary / surface boundary change") → confirm with -user before expanding. +Slice count: **4** (< 30). If S3/S4 reveals the #339/#340 router/port is not yet mergeable or its +shape differs from the ratified 7-op contract, that is a **blocking dependency**, not a #342 rescope +— surface it, do not redefine the port. ## Risk Register | Risk | Mitigation | | ---- | ---------- | -| #337/#338 not merged when Implement starts. | D1/D2 hard-block Implement on merge/rebase; PLAN designs against read contracts only. | -| #338's `DeployTargetPort` shape shifts before merge. | Design against the read interface; add a drift-watch entry; re-verify the port at Implement start. | -| Deno Deploy CLI surface/auth changes (fast-moving platform). | Freshness re-check (`deno deploy --help`) at Implement; auth flagged `NEEDS USER`; adapter isolates argv construction in one wrapper for cheap updates. | -| Unstable-API guard false-negatives (misses a transitive KV import). | Scan entrypoint + `deno.json` import map + (best-effort) `deno info` graph; document the guard's bounds; default to **refuse** on `--prod`. | -| Real e2e push needs live Deno Deploy creds/org. | Unit-test with fake `ProcessPort` (no live push); a real push is an interactive/manual verification, not a CI gate this slice, pending the auth `NEEDS USER`. | -| Scope creep into the full `deploy ` router. | D6 register-first minimal reach; router refactor is an explicit rescope trigger. | +| #339/#340 port-expansion commit not merged when #342 Implement starts. | Merge order hard-blocks Implement; PLAN designs against the ratified 7-op contract; rebase onto the port commit first. | +| Temptation to redefine the port inside #342 to "unblock." | **Forbidden** (D3): the port is owned by #339/#340. If blocked, escalate to the epic supervisor; do not fork the contract. | +| 7-op port shape lands slightly different from doctrine. | Design against ratified doctrine; drift-watch the port commit; re-verify at Implement start; reconcile with #339/#340 owner, not by local edits. | +| Deno Deploy CLI surface/auth changes (fast-moving platform). | Freshness re-check (`deno deploy --help`) at Implement; auth flagged `NEEDS USER`; adapter isolates argv construction in one wrapper. | +| Unstable-API guard false-negatives (misses a transitive KV import). | Scan entrypoint + `deno.json` import map + best-effort `deno info` graph; document bounds; default refuse on `--prod`. | +| Real e2e push needs live Deno Deploy creds/org. | Unit-test with fake `ProcessPort`; a real push is manual verification, gated on the auth `NEEDS USER`. | +| `secrets`/`rollback` re-implemented per-target (R-DEPLOY-3 violation). | Route through core conventions; adapter only maps convention → `deno deploy env`/platform rollback. | ## Anti-Patterns to Resolve or Avoid | AP | Status | Plan | | -- | ------ | ---- | -| AP-11 (side effects outside adapters) | risk | `deno deploy` shell only in `kernel/adapters/deno-deploy/**` via `ProcessPort` (D5). | -| AP-24 (switch-over-union for variants) | avoid | Register `deno-deploy` in the typed `DeployTargetRegistry`; no `if target === …` branching. | -| AP-3 (port with every backend op) | avoid | Conform to the existing 3-op `DeployTargetPort`; do not bloat it (D3). | +| AP-11 (side effects outside adapters) | risk | `deno deploy` shell only in `kernel/adapters/deno-deploy/**` via `ProcessPort` (D6). | +| AP-24 (switch-over-union for variants) | avoid | Register `deno-deploy` in the closed-on-key registry; no `if target === …` in the router (R-DEPLOY-2). | +| AP-3 (port with every backend op) | avoid | Implement the **declared subset** of the 7-op port; do not add adapter-only ops or bloat the shared port (D3/D4). | | AP-19 (undocumented permissions) | new | README permissions block: `--allow-run` + network + unstable/auth caveats (S4). | | AP-1 (command/pipeline monolith) | avoid | Adapter + wrapper + guard + resolver are separate files, each ≤ per-layer LOC caps (F-CLI-1/2). | | AP-18 (string-snapshot tests) | avoid | Assert semantic argv arrays + exit-code mapping, not stdout snapshots. | +| (R-DEPLOY-3) convention duplication | avoid | `secrets`/`rollback` consume core conventions, not adapter-local re-implementations. | ## Fitness Gates | Gate | Required | Expected evidence | | ---- | -------- | ----------------- | +| **F-DEPLOY-1** (adapter implements 7-op contract or a **declared** subset) | yes | Adapter `operations` = canonical op names; unsupported host ops explicitly declared; subset test. | +| **F-DEPLOY-2** (thin router, conventions in core) | yes | Router dispatches only (R-DEPLOY-2); `secrets`/`rollback` delegate to core (R-DEPLOY-3). | | Static (check/lint/fmt via scoped wrappers) | yes | `run-deno-check.ts`/`run-deno-lint.ts`/`run-deno-fmt.ts` on `packages/config` + `packages/cli`, `--ext ts`. | | F-1/F-CLI-1,2 (file size) | yes | Adapter ≤ 350, guard/domain ≤ 250, command ≤ 150 LOC — manual/PENDING_SCRIPT. | -| F-4/F-CLI-4 (kernel ≠ surface imports) | yes | Import-graph manual: adapter in kernel imports no `public/**`. | -| F-CLI-16 (`Deno.Command` only in adapters) | yes | Regex/manual: shelling only in `kernel/adapters/deno-deploy/**`. | -| F-CLI-31 (registry in extension-points) | yes | `DeployTargetRegistry` already exported; confirm. | +| F-4/F-CLI-4 (kernel ≠ surface imports) | yes | Import-graph manual: kernel adapter imports no `public/**`. | +| F-CLI-16 (`Deno.Command` only in adapters) | yes | Shelling only in `kernel/adapters/deno-deploy/**`. | +| F-CLI-31 (registry in extension-points) | yes | `deploy-target-registry` exported; confirm. | | F-5 (public surface audit) | yes | Only `DenoDeployTarget`/schema newly public (config); CLI adapter internal. | -| F-6 / publish dry-run (config) | yes | `deno publish --dry-run` on `@netscript/config` (published surface). | +| F-6 / publish dry-run (config) | yes | `deno publish --dry-run` on `@netscript/config`. | | F-9 / AP-19 (permissions declared) | yes | README permissions block updated. | -| F-10 (test-shape) | yes | Fake-`ProcessPort` semantic tests, guard fixtures, schema round-trip. | -| Runtime/Aspire (optional) | at evaluator | `scaffold.runtime` e2e is the merge-readiness authority (below) — evaluator/OpenHands, not per-slice. | +| F-10 (test-shape) | yes | Fake-`ProcessPort` semantic tests, guard fixtures, schema round-trip, declared-subset test. | +| Runtime/Aspire (optional) | at evaluator | `scaffold.runtime` e2e is the merge-readiness authority — evaluator, not per-slice. | + +> **F-DEPLOY-1/2 are currently seeded `reviewed`** (packages don't yet exist). This slice — the first +> cloud adapter — is where they promote toward `gated`; PLAN-EVAL confirms the promotion path. ## Arch-Debt Implications | Entry | Action | Notes | | ----- | ------ | ----- | -| `cli-deploy-artifacts-missing` (`arch-debt.md:1300`) | update | Cloud (Deno Deploy) target added; debt **advanced not closed** (no Dockerfile/k8s/compose generation; windows + deno-deploy only). | -| (new, if guard bounds are partial) | create-if-needed | If the unstable-API guard is best-effort (misses deep transitive imports), record the bound as an explicit debt entry rather than claiming full coverage. | +| `cli-deploy-artifacts-missing` (`arch-debt.md`) | update | Cloud (Deno Deploy) target added; debt **advanced not closed** (windows + deno-deploy only). | +| `Archetype-7 core-centralization + F-DEPLOY seed` | reference | This slice exercises the R-DEPLOY-3 core conventions for the first cloud adapter; note progress. | +| (new, if guard bounds are partial) | create-if-needed | If the unstable-API guard is best-effort, record the bound as explicit debt rather than claiming full coverage. | ## Validation Plan @@ -216,26 +280,27 @@ user before expanding. | 4 | CLI check | `deno run -A .llm/tools/run-deno-check.ts --root packages/cli --ext ts` (+`--unstable-kv` if KV-touching) | 0 errors | | 5 | CLI tests | `deno test` on new adapter/guard/resolver `_test.ts` (fake `ProcessPort`) | green | | 6 | Lint + fmt | `run-deno-lint.ts` + `run-deno-fmt.ts` on both roots, `--ext ts` | clean | -| 7 | Fitness (Arch-6) | `deno task arch:check` (F-CLI-*) or PENDING_SCRIPT with manual evidence | pass / documented | -| 8 | Merge-readiness (evaluator) | `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` | evaluator/OpenHands pass; NOT a per-slice gate (expensive) | -| 9 | Real push (manual, gated on auth `NEEDS USER`) | `netscript deploy deno-deploy build` (preflight, no push) then interactive push | preflight runs guard; push deferred pending auth confirmation | - -## Risks - -- Platform freshness / auth (mitigated: `NEEDS USER` + re-check + isolated argv wrapper). -- Cross-slice contract drift from unmerged #337/#338 (mitigated: D1/D2 + drift-watch + re-verify). +| 7 | Fitness (Arch-7) | `deno task arch:check` (F-DEPLOY-1/2 + F-CLI-*) or PENDING_SCRIPT with manual evidence | pass / documented | +| 8 | Merge-readiness (evaluator) | `deno task e2e:cli run scaffold.runtime --cleanup --format pretty` | evaluator pass; NOT a per-slice gate (expensive) | +| 9 | Real push (manual, gated on auth `NEEDS USER`) | `netscript deploy deno-deploy plan` (preflight, no push) then interactive `up` | preflight runs guard; push deferred pending auth confirmation | ## Dependencies -- **#337** (`feat/deploy-s1-targets-config`) — merged before Implement (Locked D1). -- **#338** (`feat/deploy-s2-doctrine`) — merged/stabilized before Implement (Locked D2). -- External: Deno 2.8 toolchain (native `deno deploy`), a Deno Deploy org + token for real push +- **#352** (`[Deploy-S1]` config contract) — **MERGED** (`44d7945b`); provides `deployTargetBaseShape` + the member spreads. +- **#357** (#338 Archetype-7 doctrine) — IMPL-EVAL PASS; merged before Implement. +- **#339/#340** (`[Deploy-S3]` bare-metal) — **owns the 3-op→7-op port-expansion commit**; #342 + rebases onto it and consumes the expanded port (does NOT redefine it). +- External: Deno 2.9 toolchain (native `deno deploy`), a Deno Deploy org + token for real push (manual verification only). ## Drift Watch -- `DeployTargetPort` shape on #338 (operations set, request/result fields) — re-verify at Implement. -- `deploy.targets.*` schema shape on #337 (base fields, `z.ZodType` annotation). +- The 7-op deploy port shape as landed by #339/#340 (op set, request/result fields) — re-verify at + Implement; reconcile with the owner, never by local port edits. +- `deploy.targets.*` schema shape on `main` (base fields, `z.ZodType` annotation) — already merged + (#352); re-confirm at Implement. - `deno deploy` CLI flag surface + non-interactive auth mechanism (docs freshness). -- If #338 adds a `deploy` operation or lands the `netscript deploy ` router, revisit - D3/D6. +- `ARCHETYPE-7-deploy-target-adapter.md` stale slice-number parenthetical (`#340 deno-deploy` / + `#342 docker`) — #338's doc to correct; #342 does not edit it. Logged so PLAN-EVAL is not misled. +- Dispatch lane: Opus 4.8 sub-agents (not WSL Codex) for this epic. diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/research.md b/.llm/tmp/run/deploy-s6-deno-deploy/research.md index 8667233c7..dd4092fe9 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/research.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/research.md @@ -1,135 +1,148 @@ # Research — deploy-s6-deno-deploy Slice **#342 [Deploy-S6]** — "Deno Deploy tier-1 adapter (MARQUEE)". Phase 2 of the deployment -epic **#327**, decision **D2**. This is a **PLAN-ONLY** run: research + plan + draft PR, then a +epic **#327**, decision **D2**. **PLAN-ONLY** run: research + plan + draft PR, then a separate-session PLAN-EVAL. No adapter/CLI/schema code is written here. +> **Revision 2 (post-FAIL_PLAN).** The first plan was returned `FAIL_PLAN` (blocking finding B1) +> because it misattributed the shipped 3-op deploy port to #338 and demoted the ratified 7-op +> doctrine to "stale corpus." This revision corrects the misattribution, conforms to the **canonical +> 7-op Archetype 7 contract**, re-baselines against **merged** `main`, and records that #342 +> **consumes** (does not own) the port expansion owned by **#339/#340**. Authoritative sources: +> `.claude/worktrees/deploy-s2/.llm/tmp/run/deploy-s2-doctrine/contract-reconciliation.md` and +> `.claude/worktrees/deploy-s3/.llm/tmp/run/deploy-s3-baremetal/port-ownership.md` (epic #327 +> supervisor decisions — internal architecture decisions, not user product decisions). + ## Re-baseline -- **Carried-in sources:** - - Deployment epic aggregation corpus: - `.claude/worktrees/deployment-research/.llm/tmp/run/epic-deployment-aggregation/` - (`deployment-architecture-spec.md`, `decision-gap-tracker.md`, `sources/deno-deploy/**`). - - **#337** landed contract on branch `feat/deploy-s1-targets-config` - (`packages/config/src/domain/schemas/deploy-schema.ts` + `config-section-types.ts`). - - **#338** target-adapter archetype in-flight on branch `feat/deploy-s2-doctrine` - (`packages/cli/src/kernel/domain/deploy/deploy-target-port.ts`, `deploy-target-registry-port.ts`, - `application/registries/deploy-target-registry.ts`, `domain/deploy/windows-service-deploy-target.ts`, - `extension-points.ts`, doctrine `07-composition-and-extension.md`). -- **Re-derived against `main`** @ `cf1ac47b` (worktree HEAD `feat/deploy-s6-deno-deploy`, forked from - origin/main cf1ac47b on 2026-07-03). -- **What changed vs the carried-in version / key correction:** The epic corpus (written before #338) - sketched a bespoke `DenoDeployAdapterPort { plan/up/down/status/logs/rollback/secrets }` shape and - a `packages/cli/src/public/features/deploy/deno-deploy/` strategy folder mirroring - `build-windows-strategy.ts`. **That is now stale.** #338 has since introduced the *real* - target-adapter archetype as a kernel **port + registry** (`DeployTargetPort` with - `operations: build|install|uninstall`, registered in `DeployTargetRegistry`, exported from - `kernel/extension-points.ts`). The adapter MUST conform to #338's `DeployTargetPort`, **not** the - corpus's speculative 7-verb port. This reconciliation is the central research correction of this - run (see Finding 3 and the operation-mapping open decision in `plan.md`). +- **Worktree base is STALE.** Branch `feat/deploy-s6-deno-deploy` forked from `cf1ac47b`, which + **predates** the deployment merges. Re-derived against **merged `origin/main` @ `37108172`** + (verified 2026-07-03). +- **What is already merged to `main` (verified firsthand):** + - **`#352`** `44d7945b [Deploy-S1] deploy.targets.* config contract (clean break)` — the + `deploy.targets.*` schema + `DeployTargetBaseSchema` now live at + `packages/config/src/domain/schemas/deploy-schema.ts` on `main`. The clean-break merge + **preserved** the spread + `z.ZodType` pattern (`deployTargetBaseShape`, + `WindowsDeployTargetSchema` spreads `...deployTargetBaseShape`, `DeployConfigSchema.targets.windows`). + → **The "#337 not merged" premise from rev 1 is dropped.** + - **`3137e455`** `Slice 2: command registry deploy port E2E green` — landed the **3-op** + `DeployTargetPort` (`build|install|uninstall`, all optional) + + `WindowsServiceDeployTarget` stub + `DeployTargetRegistry` at + `packages/cli/src/kernel/domain/deploy/**`. This is an **unrelated command-registry slice**, NOT + #338, and NOT part of the deployment epic design. → **Rev 1's misattribution corrected.** +- **What is in-flight / inbound (Implement-phase dependencies):** + - **`#357` (#338 impl)** — ratifies **Archetype 7** deploy-target-adapter doctrine (the canonical + **7-op** contract). IMPL-EVAL **PASS** at the merge gate. Doctrine file: + `.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md`. + - **`#339/#340` (Deploy-S3 bare-metal)** — **OWNS** expanding the seed 3-op `DeployTargetPort` → + the canonical **7-op** `OsServicePort`/cloud-adapter contract, and performs the verb-vocabulary + lock. It front-loads this as an **early independently-mergeable port-contract commit**. +- **Merge order for Implement:** `#357` (doctrine) → `#339/#340` port-expansion commit → **rebase + #342** onto both (plus current `main`, which already carries #352 + `3137e455`). ## Findings | # | Finding (fact the plan depends on) | How to verify | | -- | ---------------------------------- | ------------- | -| 1 | **#337 base shape.** `DeployTargetBaseSchema` is a `z.object(deployTargetBaseShape)` raw-shape spread (mode, denoPath, compileTarget, concurrency, bundle*, workspace, v8HeapMb, generateEnvFile, logging, health, docker). `WindowsDeployTargetSchema` extends it by **spread** (`{ ...deployTargetBaseShape, servyCliPath, installBase, servicePrefix }`), and `DeployConfigSchema.targets` is `z.object({ windows: WindowsDeployTargetSchema.optional() }).optional()`. New members are added as sibling keys. | `git -C show feat/deploy-s1-targets-config:packages/config/src/domain/schemas/deploy-schema.ts` | -| 2 | **#337 types.** `config-section-types.ts` declares `DeployTargetBase`, `WindowsDeployTarget extends DeployTargetBase`, `DeployConfig { targets?: { windows?: WindowsDeployTarget } }`. Schemas are annotated `z.ZodType` (so `.extend()` is hidden — spread composition is mandatory). A `deno-deploy` member needs BOTH a `DenoDeployTarget` interface and a `DenoDeployTargetSchema` typed `z.ZodType`. | `git -C show feat/deploy-s1-targets-config:packages/config/src/domain/config-section-types.ts` | -| 3 | **#338 archetype (the port my adapter implements).** `DeployTargetPort { key; label; operations: readonly ('build'\|'install'\|'uninstall')[]; build?/install?/uninstall?: (req: DeployTargetRequest) => Promise }`. `DeployTargetRequest { projectRoot; outputDir? }`. `DeployTargetResult { target; operation; message }`. Registered via `DEFAULT_DEPLOY_TARGETS` in `DeployTargetRegistry` and re-exported from `kernel/extension-points.ts`. | `git -C show feat/deploy-s2-doctrine:packages/cli/src/kernel/domain/deploy/deploy-target-port.ts` | -| 4 | **#338 windows adapter is a thin descriptor.** `WindowsServiceDeployTarget implements DeployTargetPort` with `key='windows-service'`, `label='Windows service'`, `operations=['build','install','uninstall']`; its `build/install/uninstall` currently just return a `DeployTargetResult` "registered" message and **delegate real work to the existing public deploy command pipeline** (`public/features/deploy/**`). The port is an extension-point seam layered over the still-authoritative public commands. | `git -C show feat/deploy-s2-doctrine:packages/cli/src/kernel/domain/deploy/windows-service-deploy-target.ts` | -| 5 | **Doctrine axis.** `07-composition-and-extension.md` names a **Deploy target** axis with variants `windows-service, docker, cloud (future)`, and the required extension shape: `registerDeployTarget('cloud', (ctx) => createCloudDeployFlow(ctx))` — a consumer supplies a *factory* returning a package-owned contract type; **no cross-package inheritance**. Every `Registry` subclass must be exported from `extension-points.ts` or declared internal (audit rule / F-CLI-31). | `git -C show feat/deploy-s2-doctrine:docs/architecture/doctrine/07-composition-and-extension.md` | -| 6 | **Existing public deploy router is windows-hardcoded.** `public/features/deploy/deploy-group.ts` `createDeployCommand(deps)` registers 10 cliffy verbs (build, package-cli, copy, install, start, stop, status, logs, uninstall, upgrade), description "Build and manage NetScript Windows Service deployments", calling `buildWindowsDeployment` directly with no target abstraction. Generalizing to `netscript deploy ` is net-new. | `packages/cli/src/public/features/deploy/deploy-group.ts` | -| 7 | **External-CLI shell pattern.** `ServyCliAdapter implements WindowsServicePort` shells `servy-cli.exe` via a `ProcessPort`, captures stdout/stderr, maps exit code → `{ success, message, code }`. This is the exact pattern to mirror for shelling `deno deploy` (a `ProcessPort`-driven adapter, NOT ad-hoc `Deno.Command`). | `packages/cli/src/public/adapters/servy-cli.ts` | -| 8 | **Config resolver pattern.** `deploy-config-resolvers.ts` `resolveWindowsDeploy(userDeploy?.windows)` reads the target sub-config, applies defaults, returns a `ResolvedWindowsDeployConfig`. A sibling `resolveDenoDeployTarget(userDeploy?.targets?['deno-deploy'])` follows the same shape. NOTE: #337 re-keys targets under `deploy.targets.*`, so the resolver reads `deploy.targets['deno-deploy']`, not `deploy['deno-deploy']`. | `packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts` | -| 9 | **Deno Deploy = NEW platform, source-driven, no Dockerfile.** Corpus + live docs: target is *Deno Deploy* (`console.deno.com`, `deno deploy` CLI), NOT deprecated Deploy Classic / Subhosting (deployctl, sunset 2026-07-20). Serverless microVM; build is remote 5-stage (Queue→Prepare→Install→Build→Deploy) from source; config from `deno.json`/`deno.jsonc` + dashboard. | `sources/deno-deploy/reference-builds.md`; https://docs.deno.com/runtime/reference/cli/deploy/ | -| 10 | **Runtime constraint (critical guard).** Deno Deploy runtime is pinned (corpus: Deno 2.5), runs `--allow-all`, and **rejects custom flags including `--unstable-*`**. NetScript scaffolds routinely use `--unstable-kv`. A deployed app that imports unstable APIs (e.g. Deno KV) will fail on-platform. An **unstable-API preflight guard** is required before push. | `sources/deno-deploy/reference-runtime.md`; decision-gap-tracker.md "Blockers" | -| 11 | **`deno deploy` CLI surface (live, 2026).** `deno deploy` (deploys cwd; `--prod` for production); `deno deploy create --org --app --source github\|local --runtime-mode dynamic\|static --entrypoint --framework-preset --install-command --build-command --static-dir`; `deno deploy env add [--secret]` / `deno deploy env load `; `deno deploy switch --org --app`; `deno deploy logs`; `deno deploy logout`; `deno deploy database`. | https://docs.deno.com/runtime/reference/cli/deploy/ | -| 12 | **Auth model (CI open question).** New `deno deploy` uses token-based auth stored in the OS **keyring**, auto-prompting interactively; supports **user tokens and organization tokens**. The docs page does **not** confirm a non-interactive env var (classic `deployctl` used `DENO_DEPLOY_TOKEN`). Non-interactive CI auth for the new CLI is unconfirmed → surfaced as `NEEDS USER` in the plan. | https://docs.deno.com/runtime/reference/cli/deploy/ ; https://docs.deno.com/deploy/classic/deployctl/ | -| 13 | **Existing debt this touches.** `arch-debt.md` §`cli-deploy-artifacts-missing` records that NetScript has no first-class deploy artifact generation / `netscript deploy` beyond the recorded manual facts. This slice partially advances that debt (adds a real cloud target) but does not close it. | `.llm/harness/debt/arch-debt.md:1300` | -| 14 | **`deno-deploy` naming vs registry key.** #338's windows key is `'windows-service'` (registry key) while the CLI-facing target token and the config member key are `windows`. For this slice the config member + CLI token is `deno-deploy`; the registry key should be a stable identifier (`'deno-deploy'`). Keep config-member key, CLI token, and registry key aligned to `deno-deploy` to avoid a windows-style split. | Finding 3 vs #337 `targets.windows` key | +| 1 | **Config base is merged.** `origin/main:packages/config/src/domain/schemas/deploy-schema.ts` defines `deployTargetBaseShape` (const), `DeployTargetBaseSchema: z.ZodType`, `WindowsDeployTargetSchema` spreading `...deployTargetBaseShape`, and `DeployConfigSchema.targets.windows`. New members are sibling keys added by spread (R-DEPLOY-4: no per-target base-class hierarchy). | `git show origin/main:packages/config/src/domain/schemas/deploy-schema.ts` | +| 2 | **Config types merged.** `config-section-types.ts` on `main` declares `DeployTargetBase`, `WindowsDeployTarget extends DeployTargetBase`, `DeployConfig.targets`. A `deno-deploy` member needs a `DenoDeployTarget` interface + `DenoDeployTargetSchema: z.ZodType` (explicit types — `isolatedDeclarations` + JSR slow-type). | `git show origin/main:packages/config/src/domain/config-section-types.ts` | +| 3 | **The shipped 3-op port is a SEED STUB from `3137e455`, not #338.** `deploy-target-port.ts` `DeployTargetPort { key; label; operations: ('build'\|'install'\|'uninstall')[]; build?/install?/uninstall? }`; `WindowsServiceDeployTarget` returns canned messages, no real work. It is a placeholder to be **expanded** to 7-op by #339/#340 — not the canonical contract. | `git show origin/main:packages/cli/src/kernel/domain/deploy/deploy-target-port.ts`; `git log --oneline -1 3137e455` | +| 4 | **Canonical contract = Archetype 7's uniform 7-op set** (#357, IMPL-EVAL PASS): `plan`/`emit` · `up` · `down` · `status` · `logs` · `rollback` · `secrets`. Each adapter implements the **subset it supports**; Aspire adapters delegate `up`/`plan` to Aspire; deno-deploy wraps `deno deploy`. | `.llm/harness/archetypes/ARCHETYPE-7-deploy-target-adapter.md` §"Uniform Adapter Contract" | +| 5 | **Archetype 7 is a COMPOSITE.** It composes **Archetype 2** (Integration — package-owned `OsServicePort` + cloud adapters, closed-on-key `deploy-target-registry`) and **Archetype 6** (thin `netscript deploy ` router), folding neither. Router carries **no target-specific business logic** (R-DEPLOY-2); convention-bearing primitives (health, OTEL, **secrets, rollback**) live in the **core**, shared across targets (R-DEPLOY-3). | ARCHETYPE-7 §"Composition", §"Rules" | +| 6 | **Port ownership: #339/#340 owns the 3-op→7-op expansion; #342 CONSUMES.** #342/#343 implement adapters against the expanded 7-op port and **MUST NOT redefine the port or op contract**. Verb-lock resolved: canonical = the 7 doctrine op names; legacy CLI verbs (`build/install/uninstall`) remain user-facing aliases. | `.../deploy-s3-baremetal/port-ownership.md` §Decision 1–4 | +| 7 | **External-CLI shell pattern (unchanged).** `ServyCliAdapter implements WindowsServicePort` shells `servy-cli.exe` via a `ProcessPort`, mapping exit code → result. Mirror this for shelling `deno deploy` (a `ProcessPort`-driven adapter, not ad-hoc `Deno.Command`). NOTE: `main` also has a **real** `WindowsServicePort`+`ServyCliAdapter` seam (start/stop/status bypass the port via `runServy()` today); #339/#340 unifies both seams behind the port. | `packages/cli/src/public/adapters/servy-cli.ts`; port-ownership.md §Facts | +| 8 | **Deno Deploy = NEW platform, source-driven, no Dockerfile.** Target is *Deno Deploy* (`console.deno.com`, `deno deploy` CLI), NOT deprecated Deploy Classic / Subhosting (deployctl, sunset 2026-07-20). Serverless microVM; remote 5-stage build (Queue→Prepare→Install→Build→Deploy) from source; build config from `deno.json`/`deno.jsonc` + dashboard. | https://docs.deno.com/runtime/reference/cli/deploy/ | +| 9 | **Runtime constraint (critical guard).** Deno Deploy runtime is pinned, runs `--allow-all`, and **rejects `--unstable-*` flags**. NetScript scaffolds use `--unstable-kv`. A deployed app importing unstable APIs (Deno KV) fails on-platform → an **unstable-API preflight guard** is required in `plan`/`up`. | https://docs.deno.com/... ; decision-gap-tracker.md "Blockers" | +| 10 | **`deno deploy` CLI surface (live, 2026).** `deno deploy` (deploys cwd; `--prod`); `deno deploy create --org --app --source github\|local --runtime-mode dynamic\|static --entrypoint --framework-preset --install-command --build-command --static-dir`; `deno deploy env add [--secret]` / `env load `; `deno deploy switch --org --app`; `deno deploy logs`; `deno deploy logout`; `deno deploy database`. | https://docs.deno.com/runtime/reference/cli/deploy/ | +| 11 | **Auth model (CI open question).** New `deno deploy` uses OS-keyring token-based auth, auto-prompting interactively; supports user + organization tokens. The docs do **not** confirm a non-interactive env var for the new CLI (classic `deployctl` used `DENO_DEPLOY_TOKEN`). → `NEEDS USER`. | https://docs.deno.com/... ; https://docs.deno.com/deploy/classic/deployctl/ | +| 12 | **Existing debt this advances.** `arch-debt.md` carries the deployment `Archetype-7 core-centralization + F-DEPLOY seed` entry and the `cli-deploy-artifacts-missing` / AP-1 command-registry-deploy-seams entries. This slice advances the cloud-adapter portion; it does not close them. | `.llm/harness/debt/arch-debt.md` | +| 13 | **F-DEPLOY gates are seeded `reviewed`.** `F-DEPLOY-1` (each adapter implements the 7-op contract or a declared subset) and `F-DEPLOY-2` (thin router, conventions in core) are seeded `reviewed` until the deployment packages exist, then promoted `gated`. | ARCHETYPE-7 §"Fitness Gates"; gate matrix | +| 14 | **Dispatch-lane override (this epic).** Implementers = **Opus 4.8 sub-agents only**; evaluators = separate Opus session (or Codex GPT-5.5 when reachable). **WSL Codex is dropped for the deployment epic.** Rev 1's generic "WSL Codex impl / OpenHands PLAN-EVAL" next-step is corrected. | port-ownership.md §"Dispatch-lane correction" | + +> **Slice-number note (drift):** `ARCHETYPE-7-deploy-target-adapter.md` line ~13 still carries an +> earlier parenthetical mapping (`#340 deno-deploy, #342 docker/compose`). The **current +> authoritative** mapping (this task + both rev-2 decision docs) is **#342 = Deno Deploy**. The stale +> parenthetical is #338's doc to correct (its contract-reconciliation already schedules an additive +> follow-up); #342 does not edit it. Logged so PLAN-EVAL is not misled. ## Deno Deploy deployment model (target being wrapped) -- **Platform:** the NEW **Deno Deploy** (dashboard `console.deno.com`, driven by the built-in - `deno deploy` subcommand). Deploy **Classic** (deployctl / Subhosting v1) is deprecated - (sunset 2026-07-20) and is explicitly a **non-target**. -- **Deployment unit:** an **organization → app** (the app is the deployable; deployments are its - revisions). `deno deploy switch --org --app ` sets the default context; `--org`/`--app` - are otherwise passed per-invocation. -- **Two push modes (a product default decision — see plan open-decision sweep):** - 1. **CLI push** — `deno deploy` from the project root uploads source and triggers a remote build. - Deterministic, one-shot, CI-friendly (modulo the auth question). This is the natural marquee - "one-click" for a CLI tool. - 2. **GitHub-push auto-build** — the Deno Deploy GitHub app builds on push. Zero per-deploy CLI - call, but requires repo wiring and is not a single local command. NetScript would *configure* - it, not *drive* it. -- **Build model:** remote, source-driven, **no Dockerfile**. 5 stages Queue→Prepare→Install→Build→ - Deploy. Build config comes from `deno.json`/`deno.jsonc` (app dir, framework preset, install - command, build command, pre-deploy command, runtime mode dynamic/static) plus dashboard overrides. - NetScript's job is to ensure the scaffolded project's `deno.json` carries valid Deploy build - settings and to invoke/verify the deploy — **wrap, do not reinvent** the remote builder. -- **Runtime constraints:** pinned Deno (2.5 per corpus), `--allow-all`, **no `--unstable-*`**. This - is the single most important product caveat for NetScript, whose KV-backed plugins use - `--unstable-kv`. The adapter must **preflight** for unstable-API usage and refuse/warn. -- **Auth/token:** OS-keyring token, interactive auto-prompt, user or org tokens. Non-interactive CI - token mechanism (env var) unconfirmed for the new CLI (Finding 12) → `NEEDS USER`. -- **Env vars / secrets:** `deno deploy env add [--secret]` and `deno deploy env load `, - scoped per environment (production/preview). Maps naturally onto NetScript's existing - `.env`/`.env.template` generation from the deploy build. -- **Managed data:** Deno Deploy offers managed Postgres/KV (`deno deploy database`). Wiring - NetScript's DB layer to managed PG/KV is **out of scope** for this beta slice (deferred; see - Non-goals) but is noted as a follow-up in the epic. - -## #337 / #338 dependency status - -- **#337 (`feat/deploy-s1-targets-config`)** — landed as a branch, **not yet merged to `main`**. The - `deploy.targets.*` contract this slice extends exists there. **Implementation of #342 is blocked - on #337 merging/rebasing first**; the PLAN proceeds now against the known, read contract - (Findings 1–2). Recorded as a hard dependency in `plan.md` (Locked D1). -- **#338 (`feat/deploy-s2-doctrine`)** — in-flight branch, **not merged**. It introduces the - `DeployTargetPort` + `DeployTargetRegistry` + `extension-points.ts` archetype this adapter - conforms to (Findings 3–5) and the doctrine axis text. **Implementation of #342 is blocked on #338 - merging first** (or at minimum on its port/registry surface stabilizing). PLAN designs against the - read port; PLAN-EVAL will check conformance. Recorded as Locked D2. -- **Merge ordering for the Implement phase:** #337 → #338 → rebase #342. Stated explicitly so the - Implement phase does not begin against a missing contract or a shifting port. +- **Platform:** the NEW **Deno Deploy** (dashboard `console.deno.com`, built-in `deno deploy` + subcommand). Deploy **Classic** (deployctl / Subhosting v1, sunset 2026-07-20) is a **non-target**. +- **Deployment unit:** an **organization → app** (app = deployable; deployments = its revisions). + `deno deploy switch --org --app` sets default context; otherwise `--org`/`--app` per invocation. +- **Two push modes (product default — see plan open decision):** (1) **CLI push** — `deno deploy` + from project root uploads source + triggers a remote build (deterministic, one-shot, the natural + marquee one-click); (2) **GitHub-push auto-build** — the Deno Deploy GitHub app builds on push + (NetScript would *configure*, not *drive*). +- **Build model:** remote, source-driven, **no Dockerfile**; 5 stages; build settings from + `deno.json`/`deno.jsonc` (app dir, framework preset, install/build/pre-deploy commands, runtime + mode) + dashboard. NetScript's job: ensure valid Deploy build settings + invoke/verify — **wrap, + do not reinvent** the remote builder. +- **Runtime constraints:** pinned Deno, `--allow-all`, **no `--unstable-*`** — the single most + important caveat for NetScript's KV-backed plugins → preflight guard (Finding 9). +- **Auth/token:** OS-keyring token, interactive auto-prompt, user or org tokens; non-interactive CI + path unconfirmed for the new CLI (Finding 11) → `NEEDS USER`. +- **Secrets:** `deno deploy env add [--secret]` / `env load`, per environment — mapped through the + **core secrets convention** (R-DEPLOY-3), not per-target ad-hoc code. + +## 7-op contract → Deno Deploy adapter mapping (the corrected op contract) + +Deno Deploy is a **hosted platform**: it has no OS-host install/uninstall concept, so it implements +the **declared subset** of the canonical 7-op contract it supports (R-DEPLOY-1). **`#342` does not +define these ops — it consumes the 7-op port from #339/#340 and implements this subset.** + +| Canonical op | Deno Deploy mapping | Support | +| -------------- | ------------------- | ------- | +| `plan`/`emit` | Preflight: validate `deno.json` Deploy build settings + run the **unstable-API guard**; compute the deploy plan without mutating the platform. | **Supported** (local, no remote call) | +| `up` | **`deno deploy`** (preview) / **`deno deploy --prod`** (production) — the marquee one-click push. | **Supported** (marquee) | +| `down` | Tear down = delete the Deno Deploy **app/deployment** (platform-native, via API/dashboard; CLI verb TBD). Distinct from an OS-host uninstall. | **Supported** (platform-native; CLI-surface TBD) | +| `status` | Report deployment/app state via the platform API. | **Supported** (platform API) | +| `logs` | **`deno deploy logs`** (platform API). | **Supported** | +| `rollback` | Platform-**native** rollback (repoint to a prior good deployment) — routed through the **core rollback convention**, NOT a silent no-op (R-DEPLOY-3). | **Supported** (platform mechanism) | +| `secrets` | **`deno deploy env add [--secret]` / `env load`**, via the **core secrets convention** (R-DEPLOY-3). | **Supported** | +| (host install/uninstall) | N/A — no OS-service install for a hosted platform. | **Explicitly unsupported** (declared subset) | + +## #352 / #357 (#338) / #339-#340 dependency status + +- **`#352` (Deploy-S1 config lineage) — MERGED to `main`.** `deploy.targets.*` + + `DeployTargetBaseSchema` landed (Findings 1–2). #342's config member **spreads** + `DeployTargetBaseSchema` (composition, not a new base — R-DEPLOY-4). +- **`#357` (#338 doctrine) — IMPL-EVAL PASS, at merge gate.** Ratifies Archetype 7 (7-op canonical). + #342 conforms; cite Archetype 7 by name (S-1). Implement rebases after #357 merges. +- **`#339/#340` (bare-metal) — OWNS the 3-op→7-op port-contract commit (front-loaded).** #342 + **consumes** the expanded 7-op port and rebases onto that commit; #342 does **not** redefine the + port or op vocabulary (Finding 6). +- **Merge order:** `#357` → `#339/#340` port-expansion commit → **rebase #342**. Stated so Implement + does not begin against the 3-op seed or a missing 7-op port. ## jsr-audit surface scan (package/plugin waves) -- **Surface scanned (planned):** `packages/config` public schema exports (new `DenoDeployTarget` - type + `DenoDeployTargetSchema`) and `packages/cli` `mod.ts` / `extension-points.ts` (new registry - member; no new public *type* export expected — the adapter is internal, registered via the - existing `DeployTargetRegistry`). -- **Slow-type / surface risks:** `packages/config` runs under `isolatedDeclarations`; the schema is - annotated `z.ZodType` exactly like `WindowsDeployTargetSchema` to keep fast - types. The new `DenoDeployTarget` interface must carry explicit field types (no inferred). The CLI - adapter adds no new slow-type public surface (it implements an existing port). `packages/config` - is a **published** package → `deno publish --dry-run` is a required gate for the schema slice. -- **Verdict:** low surface risk if the #337 spread-composition + `z.ZodType` annotation pattern is - followed exactly; PLAN-EVAL should confirm the annotation before slicing. +- **Surface scanned (planned):** `packages/config` new public `DenoDeployTarget` type + + `DenoDeployTargetSchema`; `packages/cli` new adapter is **internal** (registered via the registry, + no new public *type* export). +- **Slow-type / surface risks:** `packages/config` under `isolatedDeclarations` — annotate + `DenoDeployTargetSchema` as `z.ZodType` exactly like `WindowsDeployTargetSchema`; + `DenoDeployTarget` carries explicit field types. `packages/config` is **published** → + `deno publish --dry-run` is a required S1 gate. +- **Verdict:** low surface risk if the merged spread + `z.ZodType` pattern is followed; PLAN-EVAL + confirms the annotation before slicing. ## Non-goals (deferred to `stable` per D-tiers; explicit) -- **HA / multi-region / autoscaling policy** — Deno Deploy manages this; NetScript does not model it - in beta. -- **Managed Postgres/KV wiring** (`deno deploy database` → NetScript DB layer) — deferred follow-up. -- **GitHub-push auto-build orchestration** beyond, at most, emitting/validating the build settings — - CLI-push is the beta default (pending the open decision). Full GitHub-app provisioning is deferred. -- **Rollback / promote / traffic-splitting verbs** — the corpus sketched `rollback`; #338's port - exposes only `build|install|uninstall`. Beta conforms to the port; richer verbs are a follow-up - that must be coordinated with the #338 port owner (see plan open decision). -- **Deploy Classic / deployctl / Subhosting** — deprecated platform, explicitly not targeted. -- **Generalizing every existing windows verb** (start/stop/upgrade/copy/package-cli) to deno-deploy — - those are windows-service lifecycle concepts with no cloud analogue; beta implements only the - operations the `DeployTargetPort` defines. +- HA / multi-region / autoscaling policy (Deno Deploy manages it). +- Managed Postgres/KV wiring (`deno deploy database` → NetScript DB layer) — deferred follow-up. +- GitHub-push auto-build orchestration beyond emitting/validating build settings — CLI-push is the + beta default (pending open decision). +- **Owning or redefining the deploy port / op vocabulary** — owned by #339/#340; #342 consumes. +- **The `OsServicePort` + full multi-target router build** — that is #339/#340 core work; #342 adds + the deno-deploy cloud adapter behind it + the target's registry entry. +- Deploy Classic / deployctl / Subhosting — deprecated, not targeted. ## Open questions (closed in `plan.md`) -1. How do the port's `build|install|uninstall` operations map onto Deno Deploy semantics - (build=preflight/settings, install=push-deploy, uninstall=delete-app)? → plan Open-Decision + a - possible coordinated port-vocabulary note with #338. -2. CLI-push vs GitHub-push default for the beta marquee? → `NEEDS USER`. -3. Non-interactive CI auth mechanism for the new `deno deploy` (env var / org token)? → - `NEEDS USER` + freshness re-check at Implement time. -4. Does the router generalization to `netscript deploy ` land in this slice or is the - deno-deploy target reached only through the registry until #338's router work lands? → plan Locked - decision (register-first, minimal CLI reach). +1. CLI-push vs GitHub-push default for the marquee? → `NEEDS USER` (proceed CLI-push, reversible). +2. Non-interactive CI auth for the new `deno deploy`? → `NEEDS USER` + Implement-time freshness + re-check; design behind a fake `ProcessPort` so unit tests need no real auth. +3. Exact `down`/`rollback`/`status` CLI-vs-API surface (platform-native mechanisms) → confirm at + Implement against the then-current platform API; adapter isolates each op behind the port. From cf03ba6a22a1425af9d6f6527f41c9894511c0cc Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 10:14:08 +0200 Subject: [PATCH 03/14] chore(harness): record deploy-s6 rev-2 commit ledger (#342) Refs #342 #327 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012wKHquACkXnWPDgJYhhFjN --- .llm/tmp/run/deploy-s6-deno-deploy/commits.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .llm/tmp/run/deploy-s6-deno-deploy/commits.md diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md new file mode 100644 index 000000000..78a52bb72 --- /dev/null +++ b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md @@ -0,0 +1,4 @@ +# Commits — deploy-s6-deno-deploy + +- bf0beeef: docs(deploy): [Deploy-S6] research + plan for Deno Deploy tier-1 adapter (#342) — rev 1 (FAIL_PLAN B1) +- 0c760b3c: docs(deploy): [Deploy-S6] revise research+plan to Archetype-7 7-op contract (#342) — rev 2 (post-FAIL_PLAN correction) From 89512b10d26c18e57b363df9497c76e78dc283c3 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 11:50:21 +0200 Subject: [PATCH 04/14] feat(deploy): [Deploy-S6] add deno-deploy config member (S1) Add the `deploy.targets['deno-deploy']` config member to @netscript/config: a `DenoDeployTarget` interface extending `DeployTargetBase` and a `DenoDeployTargetSchema` (z.ZodType, spread-composed on `deployTargetBaseShape` per R-DEPLOY-4). Wires the `deno-deploy` key into `DeployConfigSchema.targets` and exports the new type + schema from the public surface. The explicit `z.ZodType` annotation preserves JSR fast-types. Consumes the merged base (#352); does not touch the deploy port contract. Co-Authored-By: Claude Opus 4.8 --- .../config/src/domain/config-section-types.ts | 23 +++++++++++ .../src/domain/schemas/deploy-schema.ts | 29 ++++++++++++-- packages/config/src/public/mod.ts | 2 + .../tests/schema/netscript_config_test.ts | 38 +++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) diff --git a/packages/config/src/domain/config-section-types.ts b/packages/config/src/domain/config-section-types.ts index 9eac56ba4..2a8d81a6d 100644 --- a/packages/config/src/domain/config-section-types.ts +++ b/packages/config/src/domain/config-section-types.ts @@ -461,6 +461,27 @@ export interface LinuxDeployTarget extends DeployTargetBase { runtimeDir?: string; } +/** + * Deno Deploy cloud deployment target (`deploy.targets['deno-deploy']`). + * + * Pushes a scaffolded NetScript project to the Deno Deploy platform via the + * native `deno deploy` CLI (source build, no Dockerfile). The shared compile / + * bundle fields of {@link DeployTargetBase} are inherited but generally unused + * by the hosted platform; the Deno-Deploy-specific fields below drive the push. + */ +export interface DenoDeployTarget extends DeployTargetBase { + /** Deno Deploy organization slug (`deno deploy --org`). */ + org?: string; + /** Deno Deploy application/project name (`deno deploy --app`). */ + app?: string; + /** Entrypoint module passed to `deno deploy`. Default: `main.ts`. */ + entrypoint?: string; + /** Whether pushes target production (`deno deploy --prod`) by default. */ + prod?: boolean; + /** Path to an env file loaded via `deno deploy env load`. */ + envFile?: string; +} + /** Top-level deployment configuration type. */ export interface DeployConfig { /** Deployment targets keyed by name. */ @@ -473,6 +494,8 @@ export interface DeployConfig { compose?: DockerComposeDeployTarget; /** Linux systemd deployment settings. */ linux?: LinuxDeployTarget; + /** Deno Deploy cloud deployment settings. */ + 'deno-deploy'?: DenoDeployTarget; }; } diff --git a/packages/config/src/domain/schemas/deploy-schema.ts b/packages/config/src/domain/schemas/deploy-schema.ts index ed8523a71..dbcf10fe1 100644 --- a/packages/config/src/domain/schemas/deploy-schema.ts +++ b/packages/config/src/domain/schemas/deploy-schema.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import type { + DenoDeployTarget, DeployConfig, DeployTargetBase, DockerComposeDeployTarget, @@ -174,12 +175,32 @@ export const LinuxDeployTargetSchema: z.ZodType = z.object({ runtimeDir: z.string().optional(), }); +/** + * Deno Deploy cloud deployment target (`deploy.targets['deno-deploy']`). + * Extends the shared base with Deno-Deploy-specific push fields. Consumed by + * `netscript deploy deno-deploy ` via the native `deno deploy` CLI. + */ +export const DenoDeployTargetSchema: z.ZodType = z.object({ + ...deployTargetBaseShape, + + // ── Deno Deploy platform ──────────────────────────────────────────────── + /** Deno Deploy organization slug (`deno deploy --org`). */ + org: z.string().optional(), + /** Deno Deploy application/project name (`deno deploy --app`). */ + app: z.string().optional(), + /** Entrypoint module passed to `deno deploy`. Default: 'main.ts' */ + entrypoint: z.string().optional(), + /** Whether pushes target production (`deno deploy --prod`) by default. */ + prod: z.boolean().optional(), + /** Path to an env file loaded via `deno deploy env load`. */ + envFile: z.string().optional(), +}); + /** * Top-level deploy configuration section. * `targets` is a name-keyed map of deployment targets: `windows` (Servy), - * `docker` (single-image build/push), `compose` (emit + self-host) and `linux` - * (systemd bare-metal). Future targets (deno-deploy) are added as sibling keys - * by #342–#343. + * `docker` (single-image build/push), `compose` (emit + self-host), `linux` + * (systemd bare-metal) and `deno-deploy` (Deno Deploy cloud). */ export const DeployConfigSchema: z.ZodType = z .object({ @@ -194,6 +215,8 @@ export const DeployConfigSchema: z.ZodType = z compose: DockerComposeDeployTargetSchema.optional(), /** Linux systemd deployment. */ linux: LinuxDeployTargetSchema.optional(), + /** Deno Deploy cloud deployment via the native `deno deploy` CLI. */ + 'deno-deploy': DenoDeployTargetSchema.optional(), }) .optional(), }) diff --git a/packages/config/src/public/mod.ts b/packages/config/src/public/mod.ts index a7effaafa..f841b5ce7 100644 --- a/packages/config/src/public/mod.ts +++ b/packages/config/src/public/mod.ts @@ -22,6 +22,7 @@ export { export { AppConfigSchema, DatabaseConfigSchema, + DenoDeployTargetSchema, DeployConfigSchema, DeployTargetBaseSchema, DockerComposeDeployTargetSchema, @@ -45,6 +46,7 @@ export type { DatabaseConfig, DatabaseProvider, DatabasesConfig, + DenoDeployTarget, DeployConfig, DeployTargetBase, DockerComposeDeployTarget, diff --git a/packages/config/tests/schema/netscript_config_test.ts b/packages/config/tests/schema/netscript_config_test.ts index d0f084c24..3f969ca5d 100644 --- a/packages/config/tests/schema/netscript_config_test.ts +++ b/packages/config/tests/schema/netscript_config_test.ts @@ -55,6 +55,44 @@ Deno.test('defineConfig: accepts a deploy.targets.windows target', () => { assertEquals(config.deploy?.targets?.windows?.servicePrefix, 'Acme'); }); +Deno.test('defineConfig: accepts a deploy.targets[deno-deploy] target', () => { + const config = defineConfig({ + name: 'orders', + databases: { config: [] }, + deploy: { + targets: { + 'deno-deploy': { + org: 'acme', + app: 'orders-api', + entrypoint: 'main.ts', + prod: true, + envFile: '.env.production', + }, + }, + }, + }); + + assertEquals(config.deploy?.targets?.['deno-deploy']?.org, 'acme'); + assertEquals(config.deploy?.targets?.['deno-deploy']?.app, 'orders-api'); + assertEquals(config.deploy?.targets?.['deno-deploy']?.prod, true); +}); + +Deno.test('defineConfig: accepts windows and deno-deploy targets side by side', () => { + const config = defineConfig({ + name: 'orders', + databases: { config: [] }, + deploy: { + targets: { + windows: { servicePrefix: 'Acme' }, + 'deno-deploy': { app: 'orders-api' }, + }, + }, + }); + + assertEquals(config.deploy?.targets?.windows?.servicePrefix, 'Acme'); + assertEquals(config.deploy?.targets?.['deno-deploy']?.app, 'orders-api'); +}); + Deno.test('defineConfig: does not honor the legacy deploy.windows shape (clean break)', () => { const config = defineConfig( { From 4e9481a8f3b18050f532a2a735ae9502e747c72b Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 11:50:41 +0200 Subject: [PATCH 05/14] chore(harness): record S1 commit in deploy-s6 ledger Co-Authored-By: Claude Opus 4.8 --- .llm/tmp/run/deploy-s6-deno-deploy/commits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md index 78a52bb72..33f2d0997 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md @@ -2,3 +2,4 @@ - bf0beeef: docs(deploy): [Deploy-S6] research + plan for Deno Deploy tier-1 adapter (#342) — rev 1 (FAIL_PLAN B1) - 0c760b3c: docs(deploy): [Deploy-S6] revise research+plan to Archetype-7 7-op contract (#342) — rev 2 (post-FAIL_PLAN correction) +- dc115f3a: feat(deploy): [Deploy-S6] add deno-deploy config member (S1) — config type+schema+exports+tests; check/test/lint/fmt/publish-dry-run green From 0cc7c64346962222031de8e2b1cb2c5d626ceeab Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 11:55:14 +0200 Subject: [PATCH 06/14] feat(deploy): [Deploy-S6] add unstable-API preflight guard (S2) Add a pure `scanUnstableApis` guard (D9 / research Finding 9) that flags `--unstable-*`-requiring APIs before a `deno deploy` push, since Deno Deploy rejects those flags. Detects features declared in `deno.json`'s `unstable` list and direct unstable-API tokens (Deno.openKv/cron, BroadcastChannel, Temporal) in supplied entrypoint sources. Pure by design: callers supply already-read file contents so FS access stays out of the domain (A11/F-CLI-16); the `plan` op wires this to an FS reader in S3. Best-effort (no transitive graph walk); bound recorded for arch-debt in S4. Co-Authored-By: Claude Opus 4.8 --- .../domain/deploy/unstable-api-guard.ts | Bin 0 -> 4284 bytes .../domain/deploy/unstable-api-guard_test.ts | 77 ++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts create mode 100644 packages/cli/src/kernel/domain/deploy/unstable-api-guard_test.ts diff --git a/packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts b/packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..86caa553fdbe84adc78e2ff66bfd500f11e7a2a9 GIT binary patch literal 4284 zcmai1?Q+vb6zy+6#bwHjBi7Rzkb7@fgqaA^L=U zlAgP(l@%wH872wZ)%`l>o_jSuI_l98U1?**G?$~v`AaG*Im^|2VQF5ADx+vpr3+~{ zrKF!JlVx6PhV=X~jj57G3LMf4r8A}H^mq`%99w)Yb%7sz8P`6^)EY`gqdGV`N~o%} zRX37mxtN0YUq`gQhRayj5_?PTLSTiFUC4@pEke*{056|wRY}6x z4dq2D@_ZB1GdU~pP*9R_L_Yk44VJZ83~^hTCQp+JZTR)zoTRqZrYw}UWQw{_Y;P7rl^|_W3 zugX!Ht8__8T_%JWk?D-95f#n|DwQPzg1@z>w0jB!S4yr?6Ty{Ya|lC|Q8`jVC}yEb zjzN^A4=_{6tO3b~ff33gX1Xb|8tKg|Q7$Ml0EUT=Ag9)Ge%$LJRGOg1dX9^kv~Yq5LIAF{Y9r>% zP*9@VAnT&ev!+dAIvV%nmy%Nh@nkg6N zFq6VEdRm~zA{ozPO4w;!lv2N1aaG}o;8tXgpQ2R9WNd{5P8!Qx_Jgivp1KOLn-I9~ zyte6X+ie=tyuh1>4*mi?UCW}sxio8G&^k3xiuMimW2j-md5H!qDnaf$Nq8LhZgw7* zm%4B8?cd*>aj*a~Z$$5GVUO8I4NDn2<77Tgme`y z_e1O(d4t`S4xRE`IPrV`;iT7;v!%Iz&$m8mwo(~#h7ZolO^@$a&e?9cd=5}q>met= zx{}-nJXxX!5zw63kB&r*W=hLDHDX%q@%J6_!gaI!VuLDro=>HCS|Qez@;B(YU36Iw5%hVb0Bx$lP{C5Leq%VU}rMJ|O7ESNyF z!gX2&p}=qjEOm*ZLdp~Lc|wmU@z^DYX7{h%Pli^1I(+a18QEfqOn6c_>ETvB7}K|8 zuh|P+04O@9=GB{v%gM9XXCEOhA1_}1{$_G{_4e$d69|{Cr6{0=Kw>PtmGiSNC0C(6 zJ5aOsIqO#k+V>yml-}dTE$fN5Hol&+P+m7mc|9EuF)1OA{f>V2hi-Af!_HQ-T=i); z{D!r(D)gZ(es-@VchJ~^J-QX0mg{Tt=vh^WEEUH7hS`q!$+q29AM4?9-yiz?yFpKb zoe>m+4Y`~5q;V;4(2gR%j@yESNB8D{(eLiNnm{~yvs$Naaxoe~UtVKfih{7Ux;E%w z?q?E^V(`_B+S_z`dPala#Hba3FWaqiAy=rCv&MQ*u7n(jBNnNO2W zpM+HW3Fp}NXZ=A?U|v~x=KSx}x~Ki_tRA5UVUTS;cWaBmnmr)7lGUbNBifY;%Ufrt z^|986k|fxPczR*UVq4L45OysuWP!~H+bDXR5j2?p zjhGFw4P;m!vc1v)JRTS)PTT#iirXosl3|sF1MClMtQ9#oMuZhPMTy_5v1Ag1K?WiydSclfw zO*I}tBj`ffUp#Z3M4(Q~eE#%d;~tb7uXY=_dBpIiEPpW{yWNm|EufwrO2Fj`&OfnRabcY}Z;4l$4$$~o Ro#Q=Lz@LVl1BQFo`wyG%qmBRo literal 0 HcmV?d00001 diff --git a/packages/cli/src/kernel/domain/deploy/unstable-api-guard_test.ts b/packages/cli/src/kernel/domain/deploy/unstable-api-guard_test.ts new file mode 100644 index 000000000..8e003aebb --- /dev/null +++ b/packages/cli/src/kernel/domain/deploy/unstable-api-guard_test.ts @@ -0,0 +1,77 @@ +import { assertEquals } from 'jsr:@std/assert@^1'; + +import { scanUnstableApis } from './unstable-api-guard.ts'; + +Deno.test('scanUnstableApis: clean project reports ok with no violations', () => { + const result = scanUnstableApis({ + denoJson: { tasks: {}, imports: {} }, + sources: [{ path: 'main.ts', content: 'Deno.serve(() => new Response("ok"));' }], + }); + + assertEquals(result.ok, true); + assertEquals(result.violations, []); +}); + +Deno.test('scanUnstableApis: flags Deno.openKv usage in an entrypoint', () => { + const result = scanUnstableApis({ + denoJson: {}, + sources: [{ path: 'src/main.ts', content: 'const kv = await Deno.openKv();' }], + }); + + assertEquals(result.ok, false); + assertEquals(result.violations.length, 1); + assertEquals(result.violations[0]?.api, 'Deno.openKv'); + assertEquals(result.violations[0]?.requiresFlag, '--unstable-kv'); + assertEquals(result.violations[0]?.source, 'src/main.ts'); +}); + +Deno.test('scanUnstableApis: flags features declared in deno.json unstable list', () => { + const result = scanUnstableApis({ + denoJson: { unstable: ['kv', 'cron'] }, + sources: [], + }); + + assertEquals(result.ok, false); + assertEquals(result.violations.map((v) => v.requiresFlag), [ + '--unstable-kv', + '--unstable-cron', + ]); + assertEquals(result.violations.every((v) => v.source === 'deno.json#unstable'), true); +}); + +Deno.test('scanUnstableApis: dedupes the same API across the declared list and sources', () => { + const result = scanUnstableApis({ + denoJson: { unstable: ['kv'] }, + sources: [ + { path: 'a.ts', content: 'await Deno.openKv();' }, + { path: 'a.ts', content: 'await Deno.openKv();' }, + ], + }); + + // deno.json 'kv' + one src violation for a.ts (the duplicate a.ts entry dedupes). + assertEquals(result.violations.length, 2); + assertEquals(result.ok, false); +}); + +Deno.test('scanUnstableApis: detects Temporal, cron, and BroadcastChannel tokens', () => { + const result = scanUnstableApis({ + sources: [ + { path: 'cron.ts', content: 'Deno.cron("nightly", "0 0 * * *", () => {});' }, + { path: 'time.ts', content: 'const now = Temporal.Now.instant();' }, + { path: 'bus.ts', content: 'const ch = new BroadcastChannel("events");' }, + ], + }); + + assertEquals(result.ok, false); + assertEquals(result.violations.map((v) => v.api).sort(), [ + 'BroadcastChannel', + 'Deno.cron', + 'Temporal', + ]); +}); + +Deno.test('scanUnstableApis: tolerates missing/invalid deno.json and sources', () => { + assertEquals(scanUnstableApis({}).ok, true); + assertEquals(scanUnstableApis({ denoJson: null }).ok, true); + assertEquals(scanUnstableApis({ denoJson: { unstable: 'kv' } }).ok, true); +}); From fcffd972736b26bdf16b6d5e1201b3affcb910a0 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 11:55:35 +0200 Subject: [PATCH 07/14] chore(harness): record S2 + drift/worklog in deploy-s6 ledger Co-Authored-By: Claude Opus 4.8 --- .llm/tmp/run/deploy-s6-deno-deploy/commits.md | 1 + .llm/tmp/run/deploy-s6-deno-deploy/drift.md | 35 ++++++++++++++++++ .llm/tmp/run/deploy-s6-deno-deploy/worklog.md | 36 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 .llm/tmp/run/deploy-s6-deno-deploy/drift.md create mode 100644 .llm/tmp/run/deploy-s6-deno-deploy/worklog.md diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md index 33f2d0997..e2add98a0 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md @@ -3,3 +3,4 @@ - bf0beeef: docs(deploy): [Deploy-S6] research + plan for Deno Deploy tier-1 adapter (#342) — rev 1 (FAIL_PLAN B1) - 0c760b3c: docs(deploy): [Deploy-S6] revise research+plan to Archetype-7 7-op contract (#342) — rev 2 (post-FAIL_PLAN correction) - dc115f3a: feat(deploy): [Deploy-S6] add deno-deploy config member (S1) — config type+schema+exports+tests; check/test/lint/fmt/publish-dry-run green +- 8bea12ac: feat(deploy): [Deploy-S6] add unstable-API preflight guard (S2) — pure domain scanner + 6 tests; check/test/fmt/lint green diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/drift.md b/.llm/tmp/run/deploy-s6-deno-deploy/drift.md new file mode 100644 index 000000000..5587615d3 --- /dev/null +++ b/.llm/tmp/run/deploy-s6-deno-deploy/drift.md @@ -0,0 +1,35 @@ +# Drift — deploy-s6-deno-deploy (Implement) + +Append-only. Records divergence from plan/doctrine discovered during implementation. + +## D-IMPL-1 — `secrets`/`rollback` core convention seam is ABSENT (BLOCKING for those two ops) + +- **Severity:** significant (cross-slice dependency). +- **Plan expectation:** Locked D4 sets the adapter's `operations` to the full + `['plan','up','down','status','logs','rollback','secrets']` and routes `secrets`/`rollback` + through the deploy **core** conventions (R-DEPLOY-3), owned by #339/#340/#341 (PR #364/#341). +- **Reality on branch (95576c44 S0 merge + main):** only the 7-op `DeployTargetPort` type exists. + There is **no core secrets/rollback convention primitive** anywhere in `packages/cli/src/kernel` + (grep for `rollback|secrets convention|SecretsConvention|RollbackConvention` returns only the port + type + its test + unrelated asset templates). The port's own docstring states rollback/secrets + bodies "land with the deployment hardening slice (#341); until then adapters may declare them + unsupported (omit the method) rather than provide a silent no-op." +- **Decision (per task guardrail + port doctrine):** do NOT fork the convention adapter-locally + (that is the R-DEPLOY-3 violation / B1-class error). The DenoDeployTarget declares the subset it + can genuinely support today — `operations = ['plan','up','down','status','logs']` — and OMITS the + `rollback`/`secrets` methods, exactly as the port docstring and the S0 `linux-service` test + precedent prescribe. `rollback`/`secrets` are added when the #341/#364 core convention seam lands; + the adapter is structured so they slot in by delegation, not re-implementation. +- **Surface:** flagged as a NEEDS-USER / cross-slice blocker in the implementer checkpoint. Does not + block the marquee `up` path (plan/up/down/status/logs), which is the beta driver (D5). + +## D-IMPL-2 — `packages/cli` is excluded from workspace `deno fmt`/`deno lint` + +- **Severity:** minor (evidence-path clarification, not a code change). +- Root `deno.json` `fmt.exclude` and `lint.exclude` both list `packages/cli/`; the repo `fmt:check` + task also excludes it via `^(packages/(cli)|...)`. The scoped wrappers run from the worktree root + therefore report "No target files found" for cli paths — this is the repo policy, not a failure. +- **Evidence path used instead:** `deno check` (authoritative) + `deno test` for cli, plus a direct + `deno fmt --check --no-config --line-width=100 --indent-width=2 --options-single-quote` and + `deno lint --no-config` on the new files to prove default-style conformance. `packages/config` IS + fmt/lint-gated and uses the wrappers normally. diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md b/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md new file mode 100644 index 000000000..9630a9144 --- /dev/null +++ b/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md @@ -0,0 +1,36 @@ +# Worklog — deploy-s6-deno-deploy (Implement) + +Implementer session (Opus 4.8 sub-agent, per plan D10). Separate evaluator (IMPL-EVAL) gates after. +Branch `feat/deploy-s6-deno-deploy` (PR #359, DRAFT). S0 7-op port merge (95576c44) verified present. + +## S1 — Config member (`deploy.targets['deno-deploy']`) + +- Files: `packages/config/src/domain/config-section-types.ts` (+`DenoDeployTarget`, `+'deno-deploy'` + target key), `packages/config/src/domain/schemas/deploy-schema.ts` (`DenoDeployTargetSchema: + z.ZodType` spread on `deployTargetBaseShape`; `'deno-deploy'` in + `DeployConfigSchema.targets`), `packages/config/src/public/mod.ts` (export type + schema), + `packages/config/tests/schema/netscript_config_test.ts` (2 new tests). +- Validation: check 0 errors; `deno test` 8/8 green; wrapper lint 0 findings; wrapper fmt 0 findings; + `deno publish --dry-run` on `@netscript/config` clean (only the pre-existing loader.ts + unanalyzable-dynamic-import warning; no slow-type errors on the new surface). +- Commit: dc115f3a. + +## S2 — Unstable-API preflight guard (pure domain) + +- Files: `packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts` (pure `scanUnstableApis`), + `.../unstable-api-guard_test.ts` (6 tests). +- Design: pure — caller supplies already-read `deno.json` + source contents; FS access is deferred to + an adapter (kept out of domain per A11/F-CLI-16). Detects declared `deno.json#unstable` features and + direct unstable-API tokens (`Deno.openKv`→`--unstable-kv`, `Deno.cron`, `BroadcastChannel`, + `Temporal`). Best-effort (no transitive graph walk) — bound recorded for arch-debt in S4. +- Validation: check 0 errors; `deno test` 6/6 green; direct `deno fmt --check` + `deno lint` + conformant (packages/cli is workspace-excluded from the fmt/lint gates — see drift D-IMPL-2). +- Commit: (S2, below). + +## Blocker / NEEDS-USER (see drift.md) + +- D-IMPL-1: no core `secrets`/`rollback` convention seam on branch → adapter declares the + `plan/up/down/status/logs` subset and omits rollback/secrets (per port docstring + S0 precedent); + do not fork. Surfaced, not worked around. +- D5 (CLI-push default) and non-interactive CI auth for `deno deploy` remain NEEDS-USER; proceeding on + the plan's locked CLI-push default; a real push is manual/deferred. From d2f3b93f19a46c72efbfad7ed9923c28910363e7 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 12:00:02 +0200 Subject: [PATCH 08/14] feat(deploy): add Deno Deploy target adapter (7-op subset) Implement the tier-1 Deno Deploy deployment-target adapter consuming the canonical DeployTargetPort. Declares the supported op subset plan/up/down/status/logs; rollback/secrets are omitted pending the deploy core convention seam (#341/#364) rather than forked adapter-locally. - DenoDeployTarget (domain): pure target running the unstable-API guard on plan/up; refuses production pushes with violations, warns on preview. - DenoDeployCliAdapter (kernel/adapters): confines `deno deploy` shelling behind a ProcessPort (F-CLI-16); exported argv builders unit-tested. - DenoDeployPreflightReader (kernel/adapters): FS reader feeding the pure guard with deno.json + entrypoint sources. - Register `deno-deploy` in DEFAULT_DEPLOY_TARGETS wired at the app layer. Co-Authored-By: Claude Opus 4.8 --- .../adapters/deno-deploy/deno-deploy-cli.ts | 78 ++++++++ .../deno-deploy/deno-deploy-cli_test.ts | 92 +++++++++ .../deno-deploy/deno-deploy-preflight.ts | 59 ++++++ .../registries/deploy-target-registry.ts | 15 ++ .../domain/deploy/deno-deploy-cli-port.ts | 62 +++++++ .../domain/deploy/deno-deploy-target.ts | 174 ++++++++++++++++++ .../domain/deploy/deno-deploy-target_test.ts | 138 ++++++++++++++ 7 files changed, 618 insertions(+) create mode 100644 packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts create mode 100644 packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli_test.ts create mode 100644 packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-preflight.ts create mode 100644 packages/cli/src/kernel/domain/deploy/deno-deploy-cli-port.ts create mode 100644 packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts create mode 100644 packages/cli/src/kernel/domain/deploy/deno-deploy-target_test.ts diff --git a/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts b/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts new file mode 100644 index 000000000..916b62ae5 --- /dev/null +++ b/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts @@ -0,0 +1,78 @@ +import type { ProcessPort } from '../../ports/process-port.ts'; +import type { + DenoDeployCliPort, + DenoDeployCliResult, + DenoDeployInvocation, +} from '../../domain/deploy/deno-deploy-cli-port.ts'; + +/** + * `deno deploy` CLI process wrapper (Arch-2 named-adapter-behind-port; mirrors + * the Servy CLI adapter). All `Deno.Command` shelling for Deno Deploy is + * confined here via an injected {@link ProcessPort} (F-CLI-16 / A11), so the + * domain target stays pure and the argv is unit-testable with a fake port. + * + * Note: the `delete`/`show` subcommand surface of `deno deploy` is still + * stabilizing; the argv builders below isolate that so a platform change is a + * one-file edit (see run drift). + */ +export class DenoDeployCliAdapter implements DenoDeployCliPort { + readonly #process: ProcessPort; + + constructor(process: ProcessPort) { + this.#process = process; + } + + deploy(invocation: DenoDeployInvocation): Promise { + return this.#run(buildDeployArgs(invocation), invocation.projectRoot); + } + + logs(invocation: DenoDeployInvocation): Promise { + return this.#run(buildLogsArgs(invocation), invocation.projectRoot); + } + + remove(invocation: DenoDeployInvocation): Promise { + return this.#run(buildDeleteArgs(invocation), invocation.projectRoot); + } + + status(invocation: DenoDeployInvocation): Promise { + return this.#run(buildStatusArgs(invocation), invocation.projectRoot); + } + + async #run(args: readonly string[], cwd: string): Promise { + const result = await this.#process.exec('deno', args, { cwd }); + return { code: result.code, stdout: result.stdout, stderr: result.stderr }; + } +} + +/** Shared `--org`/`--app` flag suffix used by every subcommand. */ +function targetFlags(invocation: DenoDeployInvocation): string[] { + const args: string[] = []; + if (invocation.org) args.push('--org', invocation.org); + if (invocation.app) args.push('--app', invocation.app); + return args; +} + +/** Build argv for `deno deploy [--prod] [--org] [--app] [--env-file] [entrypoint]`. */ +export function buildDeployArgs(invocation: DenoDeployInvocation): string[] { + const args = ['deploy']; + if (invocation.prod) args.push('--prod'); + args.push(...targetFlags(invocation)); + if (invocation.envFile) args.push('--env-file', invocation.envFile); + if (invocation.entrypoint) args.push(invocation.entrypoint); + return args; +} + +/** Build argv for `deno deploy logs [--org] [--app]`. */ +export function buildLogsArgs(invocation: DenoDeployInvocation): string[] { + return ['deploy', 'logs', ...targetFlags(invocation)]; +} + +/** Build argv for `deno deploy delete [--org] [--app]`. */ +export function buildDeleteArgs(invocation: DenoDeployInvocation): string[] { + return ['deploy', 'delete', ...targetFlags(invocation)]; +} + +/** Build argv for `deno deploy show [--org] [--app]`. */ +export function buildStatusArgs(invocation: DenoDeployInvocation): string[] { + return ['deploy', 'show', ...targetFlags(invocation)]; +} diff --git a/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli_test.ts b/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli_test.ts new file mode 100644 index 000000000..6e8bd2b66 --- /dev/null +++ b/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli_test.ts @@ -0,0 +1,92 @@ +import { assertEquals } from 'jsr:@std/assert@^1'; + +import type { ProcessPort, ProcessResult } from '../../ports/process-port.ts'; +import { + buildDeleteArgs, + buildDeployArgs, + buildLogsArgs, + buildStatusArgs, + DenoDeployCliAdapter, +} from './deno-deploy-cli.ts'; + +interface RecordedCall { + readonly command: string; + readonly args: readonly string[]; + readonly cwd?: string; +} + +class FakeProcess implements ProcessPort { + readonly calls: RecordedCall[] = []; + constructor(private readonly result: ProcessResult = { code: 0, stdout: '', stderr: '' }) {} + exec( + command: string, + args: readonly string[], + options?: { readonly cwd?: string }, + ): Promise { + this.calls.push({ command, args: [...args], cwd: options?.cwd }); + return Promise.resolve(this.result); + } +} + +Deno.test('buildDeployArgs: preview push maps only provided flags', () => { + assertEquals(buildDeployArgs({ projectRoot: '/p', app: 'orders' }), [ + 'deploy', + '--app', + 'orders', + ]); +}); + +Deno.test('buildDeployArgs: production push maps --prod/--org/--app/--env-file/entrypoint in order', () => { + assertEquals( + buildDeployArgs({ + projectRoot: '/p', + prod: true, + org: 'acme', + app: 'orders', + envFile: '.env.production', + entrypoint: 'main.ts', + }), + [ + 'deploy', + '--prod', + '--org', + 'acme', + '--app', + 'orders', + '--env-file', + '.env.production', + 'main.ts', + ], + ); +}); + +Deno.test('buildLogsArgs/buildDeleteArgs/buildStatusArgs: map subcommand + target flags', () => { + const inv = { projectRoot: '/p', org: 'acme', app: 'orders' }; + assertEquals(buildLogsArgs(inv), ['deploy', 'logs', '--org', 'acme', '--app', 'orders']); + assertEquals(buildDeleteArgs(inv), ['deploy', 'delete', '--org', 'acme', '--app', 'orders']); + assertEquals(buildStatusArgs(inv), ['deploy', 'show', '--org', 'acme', '--app', 'orders']); +}); + +Deno.test('DenoDeployCliAdapter: shells `deno` with deploy argv from the project root', async () => { + const process = new FakeProcess({ code: 0, stdout: 'ok', stderr: '' }); + const adapter = new DenoDeployCliAdapter(process); + + const result = await adapter.deploy({ projectRoot: '/proj', prod: true, app: 'orders' }); + + assertEquals(process.calls.length, 1); + assertEquals(process.calls[0]?.command, 'deno'); + assertEquals(process.calls[0]?.args, ['deploy', '--prod', '--app', 'orders']); + assertEquals(process.calls[0]?.cwd, '/proj'); + assertEquals(result, { code: 0, stdout: 'ok', stderr: '' }); +}); + +Deno.test('DenoDeployCliAdapter: propagates a non-zero exit code', async () => { + const process = new FakeProcess({ code: 1, stdout: '', stderr: 'auth required' }); + const adapter = new DenoDeployCliAdapter(process); + + const result = await adapter.remove({ projectRoot: '/proj', app: 'orders' }); + + assertEquals(process.calls[0]?.args, ['deploy', 'delete', '--app', 'orders']); + assertEquals(result.code, 1); + assertEquals(result.stderr, 'auth required'); +}); diff --git a/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-preflight.ts b/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-preflight.ts new file mode 100644 index 000000000..6e81186e6 --- /dev/null +++ b/packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-preflight.ts @@ -0,0 +1,59 @@ +import { join } from '@std/path'; + +import type { DenoDeployPreflightPort } from '../../domain/deploy/deno-deploy-cli-port.ts'; +import type { + UnstableApiScanInput, + UnstableApiSource, +} from '../../domain/deploy/unstable-api-guard.ts'; + +/** Default entrypoint scanned when a target declares none. */ +const DEFAULT_ENTRYPOINT = 'main.ts'; + +/** + * Filesystem reader that feeds the pure unstable-API guard (F-CLI-16 / A11). + * + * Reads `deno.json` and the deploy entrypoint from the project root, tolerating + * missing files, and hands the parsed contents to `scanUnstableApis`. The scan + * itself stays pure in the domain; this adapter owns the side effects. The read + * is best-effort (entrypoint + config, not the full transitive graph) — the + * bound is recorded in arch-debt. + */ +export class DenoDeployPreflightReader implements DenoDeployPreflightPort { + async readGuardInputs( + projectRoot: string, + entrypoint = DEFAULT_ENTRYPOINT, + ): Promise { + const denoJson = await this.#readJson(join(projectRoot, 'deno.json')) ?? + await this.#readJson(join(projectRoot, 'deno.jsonc')); + const sources = await this.#readSources(projectRoot, entrypoint); + return { denoJson, sources }; + } + + async #readJson(path: string): Promise { + const text = await this.#readText(path); + if (text === undefined) return undefined; + try { + return JSON.parse(text); + } catch { + return undefined; + } + } + + async #readSources(projectRoot: string, entrypoint: string): Promise { + const sources: UnstableApiSource[] = []; + for (const candidate of [entrypoint, join('src', entrypoint)]) { + const path = join(projectRoot, candidate); + const content = await this.#readText(path); + if (content !== undefined) sources.push({ path: candidate, content }); + } + return sources; + } + + async #readText(path: string): Promise { + try { + return await Deno.readTextFile(path); + } catch { + return undefined; + } + } +} diff --git a/packages/cli/src/kernel/application/registries/deploy-target-registry.ts b/packages/cli/src/kernel/application/registries/deploy-target-registry.ts index 3ffc324a4..7b9821767 100644 --- a/packages/cli/src/kernel/application/registries/deploy-target-registry.ts +++ b/packages/cli/src/kernel/application/registries/deploy-target-registry.ts @@ -3,6 +3,10 @@ import type { DeployTargetPort } from '../../domain/deploy/deploy-target-port.ts import type { DeployTargetRegistryPort } from '../../domain/deploy/deploy-target-registry-port.ts'; import { WindowsServiceDeployTarget } from '../../domain/deploy/windows-service-deploy-target.ts'; import { LinuxServiceDeployTarget } from '../../domain/deploy/linux-service-deploy-target.ts'; +import { DenoDeployTarget } from '../../domain/deploy/deno-deploy-target.ts'; +import { DenoDeployCliAdapter } from '../../adapters/deno-deploy/deno-deploy-cli.ts'; +import { DenoDeployPreflightReader } from '../../adapters/deno-deploy/deno-deploy-preflight.ts'; +import { DenoProcess } from '../../adapters/runtime/process/deno-process.ts'; /** Metadata and operations for a deploy target exposed by the CLI. */ export type DeployTarget = DeployTargetPort; @@ -13,11 +17,22 @@ export const WINDOWS_SERVICE_DEPLOY_TARGET: DeployTarget = new WindowsServiceDep /** Linux (systemd) service deploy target descriptor. */ export const LINUX_SERVICE_DEPLOY_TARGET: DeployTarget = new LinuxServiceDeployTarget(); +/** + * Deno Deploy cloud target descriptor. Wired with the concrete `deno deploy` + * CLI adapter (ProcessPort-backed) and the FS preflight reader at the + * application composition layer; the domain target itself imports neither. + */ +export const DENO_DEPLOY_TARGET: DeployTarget = new DenoDeployTarget({ + cli: new DenoDeployCliAdapter(new DenoProcess()), + preflight: new DenoDeployPreflightReader(), +}); + /** Ordered default deployment targets. */ export const DEFAULT_DEPLOY_TARGETS: readonly (readonly [string, DeployTarget])[] = Object .freeze([ ['windows-service', WINDOWS_SERVICE_DEPLOY_TARGET], ['linux-service', LINUX_SERVICE_DEPLOY_TARGET], + ['deno-deploy', DENO_DEPLOY_TARGET], ]); /** Registry for supported deployment targets. */ diff --git a/packages/cli/src/kernel/domain/deploy/deno-deploy-cli-port.ts b/packages/cli/src/kernel/domain/deploy/deno-deploy-cli-port.ts new file mode 100644 index 000000000..83a98c03f --- /dev/null +++ b/packages/cli/src/kernel/domain/deploy/deno-deploy-cli-port.ts @@ -0,0 +1,62 @@ +import type { UnstableApiScanInput } from './unstable-api-guard.ts'; + +/** + * Domain-facing ports for the Deno Deploy adapter (Archetype 7 / Arch-2). + * + * The `DenoDeployTarget` domain adapter depends only on these interfaces; the + * concrete implementations shell the native `deno deploy` CLI through a + * `ProcessPort` and read the filesystem — both of which live in + * `kernel/adapters/deno-deploy/**` so side effects stay at the edge + * (A11 / F-CLI-16). This mirrors how the Windows target keeps Servy shelling in + * an adapter. + */ + +/** Resolved Deno-Deploy invocation parameters mapped onto `deno deploy` flags. */ +export interface DenoDeployInvocation { + /** Project root the deploy runs from (`cwd`). */ + readonly projectRoot: string; + /** Deno Deploy organization slug (`--org`). */ + readonly org?: string; + /** Deno Deploy application/project name (`--app`). */ + readonly app?: string; + /** Whether the push targets production (`--prod`). */ + readonly prod?: boolean; + /** Entrypoint module argument passed to `deno deploy`. */ + readonly entrypoint?: string; + /** Path to an env file loaded via `deno deploy env load`. */ + readonly envFile?: string; +} + +/** Normalized result of a `deno deploy` invocation. */ +export interface DenoDeployCliResult { + /** Process exit code (0 = success). */ + readonly code: number; + /** Captured standard output. */ + readonly stdout: string; + /** Captured standard error. */ + readonly stderr: string; +} + +/** + * Port that shells the native `deno deploy` CLI. One method per deploy op the + * adapter supports; argv construction and exit-code capture live in the adapter. + */ +export interface DenoDeployCliPort { + /** `deno deploy [--prod] [--org] [--app] [--env-file] [entrypoint]`. */ + deploy(invocation: DenoDeployInvocation): Promise; + /** `deno deploy logs [--org] [--app]`. */ + logs(invocation: DenoDeployInvocation): Promise; + /** `deno deploy delete [--org] [--app]` (bring the deployment down). */ + remove(invocation: DenoDeployInvocation): Promise; + /** `deno deploy show [--org] [--app]` (report deployment status). */ + status(invocation: DenoDeployInvocation): Promise; +} + +/** + * Port that reads already-parsed project sources for the unstable-API guard, + * keeping filesystem access out of the domain. + */ +export interface DenoDeployPreflightPort { + /** Read `deno.json` + entrypoint sources for {@link UnstableApiScanInput}. */ + readGuardInputs(projectRoot: string, entrypoint?: string): Promise; +} diff --git a/packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts b/packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts new file mode 100644 index 000000000..1dbe015c5 --- /dev/null +++ b/packages/cli/src/kernel/domain/deploy/deno-deploy-target.ts @@ -0,0 +1,174 @@ +import type { + DeployTargetOperation, + DeployTargetPort, + DeployTargetRequest, + DeployTargetResult, +} from './deploy-target-port.ts'; +import type { + DenoDeployCliPort, + DenoDeployInvocation, + DenoDeployPreflightPort, +} from './deno-deploy-cli-port.ts'; +import { scanUnstableApis, type UnstableApiViolation } from './unstable-api-guard.ts'; + +/** Default deploy options baked into a {@link DenoDeployTarget} instance. */ +export interface DenoDeployTargetDefaults { + /** Deno Deploy organization slug. */ + readonly org?: string; + /** Deno Deploy application/project name. */ + readonly app?: string; + /** Whether pushes target production by default. */ + readonly prod?: boolean; + /** Entrypoint module passed to `deno deploy`. */ + readonly entrypoint?: string; + /** Path to an env file loaded via `deno deploy env load`. */ + readonly envFile?: string; +} + +/** Dependencies injected into a {@link DenoDeployTarget}. */ +export interface DenoDeployTargetDeps { + /** CLI port that shells `deno deploy`. */ + readonly cli: DenoDeployCliPort; + /** Preflight port that reads project sources for the unstable-API guard. */ + readonly preflight: DenoDeployPreflightPort; + /** Optional resolved defaults (org/app/prod/entrypoint/envFile). */ + readonly defaults?: DenoDeployTargetDefaults; +} + +/** + * Deno Deploy cloud deploy target (Archetype 7 — the tier-1 MARQUEE adapter). + * + * Implements the subset of the canonical 7-op {@link DeployTargetPort} that the + * hosted platform supports today: `plan` (preflight + unstable-API guard, no + * platform mutation), `up` (`deno deploy [--prod]` — the one-click push), + * `down` (delete the deployment), `status`, and `logs`. Host `install`/ + * `uninstall` are N/A for a hosted platform. + * + * `rollback` and `secrets` are intentionally omitted: per R-DEPLOY-3 they route + * through the deploy **core** conventions, which are not yet on `main` (owned by + * the #341/#364 hardening slice). Per the port docstring, an adapter declares + * such ops unsupported by omission rather than shipping a silent no-op or a + * forked per-target implementation — they slot in by delegation when the core + * seam lands. See run drift D-IMPL-1. + */ +export class DenoDeployTarget implements DeployTargetPort { + readonly key = 'deno-deploy'; + readonly label = 'Deno Deploy'; + readonly operations: readonly DeployTargetOperation[] = [ + 'plan', + 'up', + 'down', + 'status', + 'logs', + ]; + + readonly #cli: DenoDeployCliPort; + readonly #preflight: DenoDeployPreflightPort; + readonly #defaults: DenoDeployTargetDefaults; + + constructor(deps: DenoDeployTargetDeps) { + this.#cli = deps.cli; + this.#preflight = deps.preflight; + this.#defaults = deps.defaults ?? {}; + } + + /** + * Preflight: run the unstable-API guard against the project. Never mutates the + * platform. Reports whether the project is Deploy-safe. + */ + plan = async (request: DeployTargetRequest): Promise => { + const violations = await this.#scan(request.projectRoot); + const message = violations.length === 0 + ? `${this.label} plan: no unstable-API violations; project is Deploy-ready.` + : `${this.label} plan: ${violations.length} unstable-API violation(s) — ${ + this.#describe(violations) + }. Deno Deploy rejects these flags; resolve before \`up --prod\`.`; + return this.#result('plan', message); + }; + + /** + * The marquee one-click push: `deno deploy` (preview) / `deno deploy --prod`. + * Runs the unstable-API guard first: refuses on production pushes with + * violations, warns (but proceeds) on preview pushes. + */ + up = async (request: DeployTargetRequest): Promise => { + const invocation = this.#invocation(request); + const violations = await this.#scan(request.projectRoot); + + if (violations.length > 0 && invocation.prod) { + throw new Error( + `${this.label} up refused: production push blocked by ${violations.length} unstable-API ` + + `violation(s) — ${this.#describe(violations)}. Deno Deploy rejects --unstable-* flags.`, + ); + } + + const warning = violations.length > 0 + ? ` (warning: ${violations.length} unstable-API violation(s) — ${this.#describe(violations)})` + : ''; + const result = await this.#cli.deploy(invocation); + this.#assertOk('up', result.code, result.stderr); + return this.#result( + 'up', + `${this.label} up: ${invocation.prod ? 'production' : 'preview'} ` + + `deployment pushed for ${request.projectRoot}${warning}.`, + ); + }; + + /** Bring the deployment down (platform-native delete). */ + down = async (request: DeployTargetRequest): Promise => { + const result = await this.#cli.remove(this.#invocation(request)); + this.#assertOk('down', result.code, result.stderr); + return this.#result( + 'down', + `${this.label} down: deployment deleted for ${request.projectRoot}.`, + ); + }; + + /** Report deployment status. */ + status = async (request: DeployTargetRequest): Promise => { + const result = await this.#cli.status(this.#invocation(request)); + this.#assertOk('status', result.code, result.stderr); + return this.#result('status', result.stdout.trim() || `${this.label} status: no deployments.`); + }; + + /** Tail deployment logs. */ + logs = async (request: DeployTargetRequest): Promise => { + const result = await this.#cli.logs(this.#invocation(request)); + this.#assertOk('logs', result.code, result.stderr); + return this.#result('logs', result.stdout.trim() || `${this.label} logs: no output.`); + }; + + #invocation(request: DeployTargetRequest): DenoDeployInvocation { + return { + projectRoot: request.projectRoot, + org: this.#defaults.org, + app: this.#defaults.app, + prod: this.#defaults.prod, + entrypoint: this.#defaults.entrypoint, + envFile: this.#defaults.envFile, + }; + } + + async #scan(projectRoot: string): Promise { + const input = await this.#preflight.readGuardInputs(projectRoot, this.#defaults.entrypoint); + return scanUnstableApis(input).violations; + } + + #describe(violations: readonly UnstableApiViolation[]): string { + return violations.map((v) => `${v.api} (${v.requiresFlag})`).join(', '); + } + + #assertOk(operation: DeployTargetOperation, code: number, stderr: string): void { + if (code !== 0) { + throw new Error( + `${this.label} ${operation} failed (deno deploy exit ${code})${ + stderr.trim() ? `: ${stderr.trim()}` : '' + }`, + ); + } + } + + #result(operation: DeployTargetOperation, message: string): DeployTargetResult { + return { target: this.key, operation, message }; + } +} diff --git a/packages/cli/src/kernel/domain/deploy/deno-deploy-target_test.ts b/packages/cli/src/kernel/domain/deploy/deno-deploy-target_test.ts new file mode 100644 index 000000000..1e2a4e4b5 --- /dev/null +++ b/packages/cli/src/kernel/domain/deploy/deno-deploy-target_test.ts @@ -0,0 +1,138 @@ +import { assertEquals, assertRejects } from 'jsr:@std/assert@^1'; + +import { DenoDeployTarget, type DenoDeployTargetDefaults } from './deno-deploy-target.ts'; +import type { + DenoDeployCliPort, + DenoDeployCliResult, + DenoDeployInvocation, + DenoDeployPreflightPort, +} from './deno-deploy-cli-port.ts'; +import type { UnstableApiScanInput } from './unstable-api-guard.ts'; +import { DEFAULT_DEPLOY_TARGETS } from '../../application/registries/deploy-target-registry.ts'; + +class FakeCli implements DenoDeployCliPort { + readonly calls: { op: string; invocation: DenoDeployInvocation }[] = []; + constructor(private readonly result: DenoDeployCliResult = { code: 0, stdout: '', stderr: '' }) {} + #record(op: string, invocation: DenoDeployInvocation): Promise { + this.calls.push({ op, invocation }); + return Promise.resolve(this.result); + } + deploy(i: DenoDeployInvocation) { + return this.#record('deploy', i); + } + logs(i: DenoDeployInvocation) { + return this.#record('logs', i); + } + remove(i: DenoDeployInvocation) { + return this.#record('remove', i); + } + status(i: DenoDeployInvocation) { + return this.#record('status', i); + } +} + +class FakePreflight implements DenoDeployPreflightPort { + constructor(private readonly input: UnstableApiScanInput) {} + readGuardInputs(): Promise { + return Promise.resolve(this.input); + } +} + +function makeTarget( + input: UnstableApiScanInput, + defaults?: DenoDeployTargetDefaults, + result?: DenoDeployCliResult, +): { target: DenoDeployTarget; cli: FakeCli } { + const cli = new FakeCli(result); + const target = new DenoDeployTarget({ + cli, + preflight: new FakePreflight(input), + defaults, + }); + return { target, cli }; +} + +const CLEAN: UnstableApiScanInput = { denoJson: {}, sources: [] }; +const KV: UnstableApiScanInput = { + denoJson: {}, + sources: [{ path: 'main.ts', content: 'await Deno.openKv();' }], +}; + +Deno.test('DenoDeployTarget: declares the supported canonical op subset (no rollback/secrets)', () => { + const { target } = makeTarget(CLEAN); + assertEquals(target.key, 'deno-deploy'); + assertEquals(target.operations, ['plan', 'up', 'down', 'status', 'logs']); + assertEquals((target as { rollback?: unknown }).rollback, undefined); + assertEquals((target as { secrets?: unknown }).secrets, undefined); +}); + +Deno.test('DenoDeployTarget: plan reports guard violations without shelling', async () => { + const { target, cli } = makeTarget(KV); + const result = await target.plan({ projectRoot: '/p' }); + assertEquals(result.operation, 'plan'); + assertEquals(result.message.includes('--unstable-kv'), true); + assertEquals(cli.calls.length, 0); +}); + +Deno.test('DenoDeployTarget: plan on a clean project reports Deploy-ready', async () => { + const { target } = makeTarget(CLEAN); + const result = await target.plan({ projectRoot: '/p' }); + assertEquals(result.message.includes('Deploy-ready'), true); +}); + +Deno.test('DenoDeployTarget: up refuses a production push with unstable-API violations', async () => { + const { target, cli } = makeTarget(KV, { prod: true, app: 'orders' }); + await assertRejects(() => target.up({ projectRoot: '/p' }), Error, 'production push blocked'); + assertEquals(cli.calls.length, 0); +}); + +Deno.test('DenoDeployTarget: up proceeds on a preview push despite violations (warns)', async () => { + const { target, cli } = makeTarget(KV, { app: 'orders' }); + const result = await target.up({ projectRoot: '/p' }); + assertEquals(cli.calls[0]?.op, 'deploy'); + assertEquals(cli.calls[0]?.invocation.app, 'orders'); + assertEquals(result.message.includes('warning'), true); +}); + +Deno.test('DenoDeployTarget: up pushes cleanly and forwards resolved defaults', async () => { + const { target, cli } = makeTarget(CLEAN, { prod: true, org: 'acme', app: 'orders' }); + const result = await target.up({ projectRoot: '/p' }); + assertEquals(cli.calls[0]?.invocation, { + projectRoot: '/p', + org: 'acme', + app: 'orders', + prod: true, + entrypoint: undefined, + envFile: undefined, + }); + assertEquals(result.message.includes('production'), true); +}); + +Deno.test('DenoDeployTarget: down/status/logs delegate to the CLI port', async () => { + const { target, cli } = makeTarget(CLEAN, undefined, { + code: 0, + stdout: 'deployment-x', + stderr: '', + }); + await target.down({ projectRoot: '/p' }); + await target.status({ projectRoot: '/p' }); + await target.logs({ projectRoot: '/p' }); + assertEquals(cli.calls.map((c) => c.op), ['remove', 'status', 'logs']); +}); + +Deno.test('DenoDeployTarget: a non-zero CLI exit code throws', async () => { + const { target } = makeTarget(CLEAN, { app: 'orders' }, { + code: 1, + stdout: '', + stderr: 'boom', + }); + await assertRejects(() => target.up({ projectRoot: '/p' }), Error, 'exit 1'); +}); + +Deno.test('DEFAULT_DEPLOY_TARGETS: registers the deno-deploy target', () => { + const keys = DEFAULT_DEPLOY_TARGETS.map(([key]) => key); + assertEquals(keys.includes('deno-deploy'), true); + const entry = DEFAULT_DEPLOY_TARGETS.find(([key]) => key === 'deno-deploy'); + assertEquals(entry?.[1].key, 'deno-deploy'); + assertEquals(entry?.[1].operations, ['plan', 'up', 'down', 'status', 'logs']); +}); From 0f4a7d2add4e2780db014f8019915bd372ce2966 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 12:09:15 +0200 Subject: [PATCH 09/14] feat(deploy): wire Deno Deploy target into CLI router + config resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the Deno Deploy tier-1 target with its thin CLI reach and config resolution (Deploy-S6 S4). - resolveDenoDeployTarget: merge `deploy.targets['deno-deploy']` config with CLI flag overrides (flags win; prod defaults false) → ResolvedDenoDeployConfig. - createDenoDeployTarget: single composition factory wiring the CLI adapter + preflight reader; registry and surface both consume it (no Deno.Command leaks into features/presentation — F-CLI-16). - `netscript deploy deno-deploy `: thin router reach (R-DEPLOY-2) mapping --org/--app/--prod/--entrypoint/--env-file/--dry-run onto the resolved config; all target logic stays in the domain/adapter layers. - README permissions block (--allow-run/read/net/env) + unstable-API/auth caveats. - arch-debt: advance cli-deploy-artifacts-missing (cloud target added; rollback/ secrets + graph-complete guard + container artifacts remain). Co-Authored-By: Claude Opus 4.8 --- .llm/harness/debt/arch-debt.md | 8 +- packages/cli/README.md | 32 ++++ .../config/deploy-config-resolvers.test.ts | 68 ++++++++ .../config/deploy-config-resolvers.ts | 20 +++ .../deno-deploy/create-deno-deploy-target.ts | 25 +++ .../registries/deploy-target-registry.ts | 17 +- .../cli/src/kernel/domain/resolved-config.ts | 19 +++ .../deploy/deno-deploy/deno-deploy-command.ts | 148 ++++++++++++++++++ .../public/features/deploy/deploy-group.ts | 8 + 9 files changed, 333 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/kernel/adapters/config/deploy-config-resolvers.test.ts create mode 100644 packages/cli/src/kernel/adapters/deno-deploy/create-deno-deploy-target.ts create mode 100644 packages/cli/src/public/features/deploy/deno-deploy/deno-deploy-command.ts diff --git a/.llm/harness/debt/arch-debt.md b/.llm/harness/debt/arch-debt.md index 80fa239b5..e84d545d6 100644 --- a/.llm/harness/debt/arch-debt.md +++ b/.llm/harness/debt/arch-debt.md @@ -1313,12 +1313,18 @@ match the merged exemplars). IMPL-EVAL must not FAIL a slice for retaining eithe command. - **Owner:** Deployment tooling follow-up. - **Created:** 2026-06-22. -- **Status:** partially resolved (#339 + #340), narrowed to container/orchestrator artifacts. +- **Status:** partially resolved (#339 + #340, #342), narrowed to container/orchestrator artifacts. - **Update (2026-07-03, #339/#340):** `netscript deploy` now IS a first-class command that emits bare-metal deployment artifacts: a self-contained single binary per service (`deno compile`, OS-generic triple) managed as an OS service through the OS-agnostic `OsServicePort` (Servy on Windows, systemd on Linux). Dockerfiles / Compose / Kubernetes manifests are still not generated — the container/orchestrator lane is tracked by the Aspire-compose deploy adapter (#343). +- **Update (2026-07-03, #342 Deploy-S6):** a first-class cloud deploy target now exists — + `netscript deploy deno-deploy` (`plan`/`up`/`down`/`status`/`logs`) via the `DenoDeployTarget` + adapter behind the canonical `DeployTargetPort`. `rollback`/`secrets` remain declared-unsupported + pending the deploy-core convention seam (#341/#364) — the adapter delegates to the seam by design + and omits the ops until it lands, rather than forking. The unstable-API preflight guard is + best-effort (`deno.json` + entrypoint, not the full transitive module graph). - **Gate:** Close when `netscript deploy` (or scaffold generation) also emits the container / orchestrator artifacts, or the deployment docs are rewritten to remove generated-artifact expectations for those targets. diff --git a/packages/cli/README.md b/packages/cli/README.md index d84a2c712..7421729e1 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -71,6 +71,38 @@ The bare-metal targets implement the canonical `plan` / `emit` / `up` / `down` / --- +## ☁️ Deno Deploy target + +`netscript deploy deno-deploy ` deploys a workspace to +[Deno Deploy](https://deno.com/deploy). Supported operations: `plan` (preflight +only), `up` (`deno deploy [--prod]`), `down`, `status`, `logs`. Configure defaults +under `deploy.targets['deno-deploy']` in `netscript.config.ts` +(`org`/`app`/`prod`/`entrypoint`/`envFile`) or pass +`--org`/`--app`/`--prod`/`--entrypoint`/`--env-file`; flags override config. `up +--dry-run` runs `plan` without pushing. + +`plan`/`up` first run an unstable-API guard: `up --prod` **refuses** to push when +the project uses APIs that require `--unstable-*` flags (e.g. `Deno.openKv`, +`Deno.cron`, `BroadcastChannel`, `Temporal`), which Deno Deploy rejects; preview +pushes warn but proceed. + +### Permissions + +The deploy subcommands shell out to the `deno deploy` CLI and read project files, +so a host binary embedding this surface must grant: + +| Permission | Why | +| -------------- | ------------------------------------------------------------- | +| `--allow-run` | Spawn the `deno deploy` CLI (`plan`/`up`/`down`/`status`/`logs`). | +| `--allow-read` | Read `deno.json` + the entrypoint for the unstable-API guard. | +| `--allow-net` | The `deno deploy` CLI uploads to and queries the platform. | +| `--allow-env` | The `deno deploy` CLI reads auth/token environment variables. | + +Authentication is delegated to the `deno deploy` CLI (its token/login flow); this +surface issues no credentials of its own. + +--- + ## 📖 Documentation - **Reference**: [rickylabs.github.io/netscript/reference/cli/](https://rickylabs.github.io/netscript/reference/cli/) diff --git a/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.test.ts b/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.test.ts new file mode 100644 index 000000000..061cee251 --- /dev/null +++ b/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.test.ts @@ -0,0 +1,68 @@ +import { assertEquals } from 'jsr:@std/assert@^1'; +import type { DeployConfig } from '@netscript/config'; + +import { resolveDenoDeployTarget } from './deploy-config-resolvers.ts'; + +Deno.test('resolveDenoDeployTarget: empty config defaults prod to false with no fields', () => { + assertEquals(resolveDenoDeployTarget(undefined), { + org: undefined, + app: undefined, + prod: false, + entrypoint: undefined, + envFile: undefined, + }); +}); + +Deno.test('resolveDenoDeployTarget: reads deploy.targets[deno-deploy] from config', () => { + const deploy = { + targets: { + 'deno-deploy': { + org: 'acme', + app: 'orders', + prod: true, + entrypoint: 'main.ts', + envFile: '.env.production', + }, + }, + } as unknown as DeployConfig; + + assertEquals(resolveDenoDeployTarget(deploy), { + org: 'acme', + app: 'orders', + prod: true, + entrypoint: 'main.ts', + envFile: '.env.production', + }); +}); + +Deno.test('resolveDenoDeployTarget: CLI flag overrides win over config', () => { + const deploy = { + targets: { + 'deno-deploy': { org: 'acme', app: 'orders', prod: false }, + }, + } as unknown as DeployConfig; + + assertEquals( + resolveDenoDeployTarget(deploy, { app: 'orders-staging', prod: true }), + { + org: 'acme', + app: 'orders-staging', + prod: true, + entrypoint: undefined, + envFile: undefined, + }, + ); +}); + +Deno.test('resolveDenoDeployTarget: flags alone work with no config section', () => { + assertEquals( + resolveDenoDeployTarget(undefined, { org: 'acme', app: 'orders' }), + { + org: 'acme', + app: 'orders', + prod: false, + entrypoint: undefined, + envFile: undefined, + }, + ); +}); diff --git a/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts b/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts index fafb16a05..cd0b306a4 100644 --- a/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts +++ b/packages/cli/src/kernel/adapters/config/deploy-config-resolvers.ts @@ -24,6 +24,7 @@ import type { RegisteredPluginConfig, ResolvedAppConfig, ResolvedDefaultsConfig, + ResolvedDenoDeployConfig, ResolvedDeployBaseConfig, ResolvedLinuxDeployConfig, ResolvedPluginConfig, @@ -351,6 +352,25 @@ export function resolveLinuxDeploy(userDeploy?: DeployConfig): ResolvedLinuxDepl }; } +/** + * Resolve the Deno Deploy target config by merging netscript.config.ts + * `deploy.targets['deno-deploy']` with CLI flag overrides. Overrides (flags) + * take precedence over config; `prod` defaults to `false`. + */ +export function resolveDenoDeployTarget( + userDeploy?: DeployConfig, + overrides?: Partial, +): ResolvedDenoDeployConfig { + const target = userDeploy?.targets?.['deno-deploy']; + return { + org: overrides?.org ?? target?.org, + app: overrides?.app ?? target?.app, + prod: overrides?.prod ?? target?.prod ?? false, + entrypoint: overrides?.entrypoint ?? target?.entrypoint, + envFile: overrides?.envFile ?? target?.envFile, + }; +} + // ============================================================================ // PROJECT ROOT FINDER // ============================================================================ diff --git a/packages/cli/src/kernel/adapters/deno-deploy/create-deno-deploy-target.ts b/packages/cli/src/kernel/adapters/deno-deploy/create-deno-deploy-target.ts new file mode 100644 index 000000000..e5ee1df7f --- /dev/null +++ b/packages/cli/src/kernel/adapters/deno-deploy/create-deno-deploy-target.ts @@ -0,0 +1,25 @@ +import { + DenoDeployTarget, + type DenoDeployTargetDefaults, +} from '../../domain/deploy/deno-deploy-target.ts'; +import { DenoDeployCliAdapter } from './deno-deploy-cli.ts'; +import { DenoDeployPreflightReader } from './deno-deploy-preflight.ts'; +import { DenoProcess } from '../runtime/process/deno-process.ts'; + +/** + * Compose a {@link DenoDeployTarget} wired with the concrete `deno deploy` CLI + * adapter (ProcessPort-backed) and the filesystem preflight reader. + * + * This is the single composition point for the Deno Deploy target: the default + * registry uses it with no baked defaults, and the CLI surface uses it with the + * config-resolved defaults (org/app/prod/entrypoint/envFile). Keeping the wiring + * here means neither the registry nor the command surface reaches for + * `Deno.Command` (F-CLI-16); they consume a ready target. + */ +export function createDenoDeployTarget(defaults?: DenoDeployTargetDefaults): DenoDeployTarget { + return new DenoDeployTarget({ + cli: new DenoDeployCliAdapter(new DenoProcess()), + preflight: new DenoDeployPreflightReader(), + defaults, + }); +} diff --git a/packages/cli/src/kernel/application/registries/deploy-target-registry.ts b/packages/cli/src/kernel/application/registries/deploy-target-registry.ts index 7b9821767..5999d63a1 100644 --- a/packages/cli/src/kernel/application/registries/deploy-target-registry.ts +++ b/packages/cli/src/kernel/application/registries/deploy-target-registry.ts @@ -3,10 +3,7 @@ import type { DeployTargetPort } from '../../domain/deploy/deploy-target-port.ts import type { DeployTargetRegistryPort } from '../../domain/deploy/deploy-target-registry-port.ts'; import { WindowsServiceDeployTarget } from '../../domain/deploy/windows-service-deploy-target.ts'; import { LinuxServiceDeployTarget } from '../../domain/deploy/linux-service-deploy-target.ts'; -import { DenoDeployTarget } from '../../domain/deploy/deno-deploy-target.ts'; -import { DenoDeployCliAdapter } from '../../adapters/deno-deploy/deno-deploy-cli.ts'; -import { DenoDeployPreflightReader } from '../../adapters/deno-deploy/deno-deploy-preflight.ts'; -import { DenoProcess } from '../../adapters/runtime/process/deno-process.ts'; +import { createDenoDeployTarget } from '../../adapters/deno-deploy/create-deno-deploy-target.ts'; /** Metadata and operations for a deploy target exposed by the CLI. */ export type DeployTarget = DeployTargetPort; @@ -18,14 +15,12 @@ export const WINDOWS_SERVICE_DEPLOY_TARGET: DeployTarget = new WindowsServiceDep export const LINUX_SERVICE_DEPLOY_TARGET: DeployTarget = new LinuxServiceDeployTarget(); /** - * Deno Deploy cloud target descriptor. Wired with the concrete `deno deploy` - * CLI adapter (ProcessPort-backed) and the FS preflight reader at the - * application composition layer; the domain target itself imports neither. + * Deno Deploy cloud target descriptor. Composed with the concrete `deno deploy` + * CLI adapter (ProcessPort-backed) and the FS preflight reader; the registry + * instance carries no baked defaults (the CLI surface builds a config-resolved + * instance per invocation). */ -export const DENO_DEPLOY_TARGET: DeployTarget = new DenoDeployTarget({ - cli: new DenoDeployCliAdapter(new DenoProcess()), - preflight: new DenoDeployPreflightReader(), -}); +export const DENO_DEPLOY_TARGET: DeployTarget = createDenoDeployTarget(); /** Ordered default deployment targets. */ export const DEFAULT_DEPLOY_TARGETS: readonly (readonly [string, DeployTarget])[] = Object diff --git a/packages/cli/src/kernel/domain/resolved-config.ts b/packages/cli/src/kernel/domain/resolved-config.ts index efc8657a0..a2e7bd5a5 100644 --- a/packages/cli/src/kernel/domain/resolved-config.ts +++ b/packages/cli/src/kernel/domain/resolved-config.ts @@ -260,6 +260,25 @@ export interface ResolvedLinuxDeployConfig extends ResolvedDeployBaseConfig { runtimeDir: string; } +/** + * Resolved Deno Deploy target configuration. + * Merges `deploy.targets['deno-deploy']` from netscript.config.ts with CLI flag + * overrides (flags win). Consumed by the composition layer to construct a + * defaults-baked `DenoDeployTarget`. + */ +export interface ResolvedDenoDeployConfig { + /** Deno Deploy organization slug (`--org`). */ + org?: string; + /** Deno Deploy application/project name (`--app`). */ + app?: string; + /** Whether pushes target production by default (`--prod`). */ + prod: boolean; + /** Entrypoint module passed to `deno deploy`. */ + entrypoint?: string; + /** Path to an env file loaded via `--env-file`. */ + envFile?: string; +} + /** * Fully-resolved deployment configuration — the single source of truth * passed to all adapters, generators, and command handlers. diff --git a/packages/cli/src/public/features/deploy/deno-deploy/deno-deploy-command.ts b/packages/cli/src/public/features/deploy/deno-deploy/deno-deploy-command.ts new file mode 100644 index 000000000..18fdd225b --- /dev/null +++ b/packages/cli/src/public/features/deploy/deno-deploy/deno-deploy-command.ts @@ -0,0 +1,148 @@ +/** + * @module commands/deploy/deno-deploy + * + * `netscript deploy deno-deploy ` — dispatch the Deno Deploy cloud target's + * operations (`plan`/`up`/`down`/`status`/`logs`). + * + * This surface is deliberately thin (R-DEPLOY-2): it maps CLI flags + * (`--org`/`--app`/`--prod`/`--entrypoint`/`--env-file`/`--dry-run`) onto the + * resolved target config and delegates to the domain target. All target logic — + * the unstable-API guard, `deno deploy` argv, production-push refusal — lives in + * the domain/adapter layers. No `Deno.Command` is issued here (F-CLI-16); the + * target is composed via `createDenoDeployTarget`. + */ + +import { Command } from '@cliffy/command'; +import { red } from '@std/fmt/colors'; + +import { failDeployCommand } from '../../../../kernel/adapters/deploy/deploy-exit.ts'; +import { outputError, outputText } from '../../../../kernel/presentation/output/default-output.ts'; +import { createDenoDeployTarget } from '../../../../kernel/adapters/deno-deploy/create-deno-deploy-target.ts'; +import { resolveDenoDeployTarget } from '../../../../kernel/adapters/config/deploy-config-resolvers.ts'; +import type { + DeployTargetOperation, + DeployTargetPort, +} from '../../../../kernel/domain/deploy/deploy-target-port.ts'; +import type { PublicCommandDependencies } from '../../root/public-command-dependencies.ts'; + +/** Dependencies the Deno Deploy command surface needs. */ +type DenoDeployCommandDependencies = Pick< + PublicCommandDependencies, + 'loadConfig' | 'resolveProjectRoot' +>; + +/** Flag overrides shared by every Deno Deploy operation. */ +interface DenoDeployFlags { + readonly org?: string; + readonly app?: string; + readonly prod?: boolean; + readonly entrypoint?: string; + readonly envFile?: string; + readonly projectRoot?: string; +} + +/** Resolve the project root, best-effort config, and dispatch a single op. */ +async function dispatch( + operation: DeployTargetOperation, + dependencies: DenoDeployCommandDependencies, + flags: DenoDeployFlags, +): Promise { + const projectRoot = (await dependencies.resolveProjectRoot(flags.projectRoot)) ?? Deno.cwd(); + + // Merge netscript.config.ts `deploy.targets['deno-deploy']` with CLI flags + // (flags win). Config is optional — flags alone are sufficient. + let deploySection; + try { + const config = await dependencies.loadConfig({ cwd: projectRoot }); + deploySection = config.deploy; + } catch { + deploySection = undefined; + } + + const resolved = resolveDenoDeployTarget(deploySection, { + org: flags.org, + app: flags.app, + prod: flags.prod, + entrypoint: flags.entrypoint, + envFile: flags.envFile, + }); + + const target: DeployTargetPort = createDenoDeployTarget(resolved); + const handler = target[operation]; + if (!handler) { + outputError(red(`✗ Deno Deploy does not support \`${operation}\`.`)); + failDeployCommand('Deploy command failed.'); + } + + try { + const result = await handler({ projectRoot }); + outputText(result.message); + } catch (error: unknown) { + outputError(red(`✗ ${error instanceof Error ? error.message : String(error)}`)); + failDeployCommand('Deploy command failed.'); + } +} + +/** Attach the flags shared by every Deno Deploy subcommand. */ +function withDenoDeployFlags( + command: Command, +): Command { + return command + .option('--org ', 'Deno Deploy organization slug') + .option('--app ', 'Deno Deploy application/project name') + .option('--entrypoint ', 'Entrypoint module passed to `deno deploy`') + .option('--env-file ', 'Env file loaded via --env-file') + .option('--project-root ', 'Project root (defaults to discovered root or cwd)'); +} + +/** Create the `netscript deploy deno-deploy` command group. */ +export function createDenoDeployCommand( + dependencies: DenoDeployCommandDependencies, +): Command { + const toFlags = (options: Record): DenoDeployFlags => ({ + org: options.org as string | undefined, + app: options.app as string | undefined, + prod: options.prod as boolean | undefined, + entrypoint: options.entrypoint as string | undefined, + envFile: options.envFile as string | undefined, + projectRoot: options.projectRoot as string | undefined, + }); + + const plan = withDenoDeployFlags( + new Command() + .name('plan') + .description('Preflight the project for Deno Deploy (unstable-API guard; no push)'), + ).action((options) => dispatch('plan', dependencies, toFlags(options))); + + const up = withDenoDeployFlags( + new Command() + .name('up') + .description('Push a deployment (`deno deploy [--prod]`)') + .option('--prod', 'Push to production') + .option('--dry-run', 'Preflight only (equivalent to `plan`); do not push'), + ).action((options) => dispatch(options.dryRun ? 'plan' : 'up', dependencies, toFlags(options))); + + const down = withDenoDeployFlags( + new Command().name('down').description('Delete the deployment'), + ).action((options) => dispatch('down', dependencies, toFlags(options))); + + const status = withDenoDeployFlags( + new Command().name('status').description('Show deployment status'), + ).action((options) => dispatch('status', dependencies, toFlags(options))); + + const logs = withDenoDeployFlags( + new Command().name('logs').description('Show deployment logs'), + ).action((options) => dispatch('logs', dependencies, toFlags(options))); + + return new Command() + .name('deno-deploy') + .description('Deploy to Deno Deploy (cloud target)') + .action(function () { + this.showHelp(); + }) + .command('plan', plan) + .command('up', up) + .command('down', down) + .command('status', status) + .command('logs', logs); +} diff --git a/packages/cli/src/public/features/deploy/deploy-group.ts b/packages/cli/src/public/features/deploy/deploy-group.ts index 96f68c9f9..c76ff1383 100644 --- a/packages/cli/src/public/features/deploy/deploy-group.ts +++ b/packages/cli/src/public/features/deploy/deploy-group.ts @@ -2,6 +2,7 @@ import { Command } from '@cliffy/command'; import { buildWindowsDeployment } from './build/build-windows-strategy.ts'; import { createDeployBuildCommand } from './build/build-deploy-command.ts'; +import { createDenoDeployCommand } from './deno-deploy/deno-deploy-command.ts'; import { copyCommand } from './copy/copy-deploy-command.ts'; import { createDeployInstallCommand } from './install/install-deploy-command.ts'; import { logsCommand } from './logs/logs-deploy-command.ts'; @@ -39,6 +40,13 @@ export function createDeployCommand( }, }), ) + .command( + 'deno-deploy', + createDenoDeployCommand({ + loadConfig: dependencies.loadConfig, + resolveProjectRoot: dependencies.resolveProjectRoot, + }), + ) .command('package-cli', packageCliCommand) .command('copy', copyCommand) .command( From c7ed28a34d3ab6a13ff963e032d659002eb4ade1 Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 12:10:02 +0200 Subject: [PATCH 10/14] chore(harness): record Deploy-S6 S3+S4 in run artifacts Co-Authored-By: Claude Opus 4.8 --- .llm/tmp/run/deploy-s6-deno-deploy/commits.md | 2 + .llm/tmp/run/deploy-s6-deno-deploy/worklog.md | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md index e2add98a0..6a6e5c5eb 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/commits.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/commits.md @@ -4,3 +4,5 @@ - 0c760b3c: docs(deploy): [Deploy-S6] revise research+plan to Archetype-7 7-op contract (#342) — rev 2 (post-FAIL_PLAN correction) - dc115f3a: feat(deploy): [Deploy-S6] add deno-deploy config member (S1) — config type+schema+exports+tests; check/test/lint/fmt/publish-dry-run green - 8bea12ac: feat(deploy): [Deploy-S6] add unstable-API preflight guard (S2) — pure domain scanner + 6 tests; check/test/fmt/lint green +- 06f595ff: feat(deploy): add Deno Deploy target adapter (7-op subset) (S3) — DenoDeployTarget (domain) + DenoDeployCliAdapter/DenoDeployPreflightReader (kernel/adapters, ProcessPort-behind-port) + registry wiring; 14 tests; check/test/fmt/lint green; pushed 34013a72..06f595ff +- 2c93ce8d: feat(deploy): wire Deno Deploy target into CLI router + config resolver (S4) — resolveDenoDeployTarget + createDenoDeployTarget composition factory + `netscript deploy deno-deploy` thin router (plan/up/down/status/logs) + README permissions block + arch-debt advance; +4 resolver tests (18 total); check(72 files)/test/fmt green diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md b/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md index 9630a9144..2c5cca793 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/worklog.md @@ -27,6 +27,55 @@ Branch `feat/deploy-s6-deno-deploy` (PR #359, DRAFT). S0 7-op port merge (95576c conformant (packages/cli is workspace-excluded from the fmt/lint gates — see drift D-IMPL-2). - Commit: (S2, below). +## S3 — Adapter (7-op subset) + process wrapper + registry + +- Files (domain): `packages/cli/src/kernel/domain/deploy/deno-deploy-cli-port.ts` (ports: + `DenoDeployCliPort` deploy/logs/remove/status + `DenoDeployPreflightPort.readGuardInputs`), + `.../deno-deploy-target.ts` (`DenoDeployTarget implements DeployTargetPort`; + key `deno-deploy`, operations `plan/up/down/status/logs`), `.../deno-deploy-target_test.ts` (9 tests). +- Files (kernel/adapters): `packages/cli/src/kernel/adapters/deno-deploy/deno-deploy-cli.ts` + (`DenoDeployCliAdapter implements DenoDeployCliPort`, shells `deno deploy` through injected + `ProcessPort` — F-CLI-16; exported `buildDeployArgs`/`buildLogsArgs`/`buildDeleteArgs`/`buildStatusArgs`), + `.../deno-deploy-preflight.ts` (`DenoDeployPreflightReader`, FS read of `deno.json`+entrypoint feeding + the pure guard), `.../deno-deploy-cli_test.ts` (5 tests, exact argv + exit-code mapping). +- Files (application): `packages/cli/src/kernel/application/registries/deploy-target-registry.ts` + (`DENO_DEPLOY_TARGET` wired with concrete adapters at the composition layer; added to + `DEFAULT_DEPLOY_TARGETS`). Domain target imports neither `Deno.Command` nor FS. +- Behaviour: `plan` runs the guard, never mutates; `up` runs the guard then `deno deploy [--prod]` — + **refuses** production pushes with unstable-API violations (throw), warns-but-proceeds on preview; + `down`/`status`/`logs` delegate to the CLI port; non-zero exit → throw with stderr. rollback/secrets + omitted (D-IMPL-1). +- Validation: scoped `run-deno-check` 0 errors across domain/deploy + adapters/deno-deploy + + application/registries (22 files); `deno test` 14/14 green; direct `deno fmt --check` + `deno lint` + conformant (packages/cli workspace-excluded from the fmt/lint gates — drift D-IMPL-2). +- Commit: 06f595ff; pushed 34013a72..06f595ff. + +## S4 — Config resolver + thin CLI router reach + README/debt + +- Files (domain): `packages/cli/src/kernel/domain/resolved-config.ts` (+`ResolvedDenoDeployConfig`). +- Files (kernel/adapters): `.../adapters/config/deploy-config-resolvers.ts` (+`resolveDenoDeployTarget`: + merge `deploy.targets['deno-deploy']` config with CLI flag overrides, flags win, `prod` default + false), `.../adapters/config/deploy-config-resolvers.test.ts` (4 tests), + `.../adapters/deno-deploy/create-deno-deploy-target.ts` (single composition factory wiring CLI + adapter + preflight reader; registry refactored to use it). +- Files (surface): `.../public/features/deploy/deno-deploy/deno-deploy-command.ts` + (`createDenoDeployCommand` — thin `deno-deploy plan|up|down|status|logs` group mapping + `--org/--app/--prod/--entrypoint/--env-file/--dry-run` onto the resolved config; `up --dry-run` + diverts to `plan`; no target logic in the surface, no `Deno.Command`), wired into + `deploy-group.ts`. +- Docs/debt: `packages/cli/README.md` (Deno Deploy target section + permissions table + `--allow-run/read/net/env` + unstable-API/auth caveats), `.llm/harness/debt/arch-debt.md` + (`cli-deploy-artifacts-missing` advanced, not closed). +- Validation: scoped `run-deno-check` 0 errors across 72 files (adapters/deno-deploy + adapters/config + + domain + public/features/deploy + application/registries); `deno test` 18/18 green (4 resolver + + 5 adapter + 9 target); direct `deno fmt --check` conformant on all 7 S4 TS files. +- Note (lint): the surface's cliffy `Command` generics are the repo-standard, explicitly + accepted "top-level router any" (packages/cli is excluded from the lint gate); `deno lint + --no-config` flags them, but they match the established pattern in every `features/**` command and + are not new type-soundness debt. No `any` in business logic (flag marshalling uses `unknown` + + narrow casts). +- Commit: 2c93ce8d. + ## Blocker / NEEDS-USER (see drift.md) - D-IMPL-1: no core `secrets`/`rollback` convention seam on branch → adapter declares the From 913235417f50c33faaec4066a2e47e69630a86fd Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 12:11:03 +0200 Subject: [PATCH 11/14] chore(harness): record Deploy-S6 drift D-IMPL-3/D-IMPL-4 + S4 lint note Co-Authored-By: Claude Opus 4.8 --- .llm/tmp/run/deploy-s6-deno-deploy/drift.md | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.llm/tmp/run/deploy-s6-deno-deploy/drift.md b/.llm/tmp/run/deploy-s6-deno-deploy/drift.md index 5587615d3..8c0d1fc63 100644 --- a/.llm/tmp/run/deploy-s6-deno-deploy/drift.md +++ b/.llm/tmp/run/deploy-s6-deno-deploy/drift.md @@ -33,3 +33,26 @@ Append-only. Records divergence from plan/doctrine discovered during implementat `deno fmt --check --no-config --line-width=100 --indent-width=2 --options-single-quote` and `deno lint --no-config` on the new files to prove default-style conformance. `packages/config` IS fmt/lint-gated and uses the wrappers normally. +- **S4 addendum (`no-explicit-any`):** `deno lint --no-config` on the new `deno-deploy-command.ts` + reports `no-explicit-any` for the cliffy `Command` generics. These are the repo-standard, + explicitly-accepted "top-level router any" (see MEMORY: only 2 accepted casts + the router `any`); + every command in `packages/cli/src/public/features/**` uses the identical signature. cli is + lint-excluded, so this is not a new soundness debt and not a gate failure. Business logic uses no + `any` (flag marshalling narrows from `unknown`). + +## D-IMPL-3 — unstable-API preflight guard is best-effort (not full graph walk) + +- **Severity:** minor (documented bound, recorded in arch-debt). +- The `DenoDeployPreflightReader` feeds the pure guard with `deno.json` + the entrypoint (and + `src/`), not the full transitive module graph. A violation reached only through a deep + import is not detected. This is an intentional cost/scope bound for S6; recorded under + `cli-deploy-artifacts-missing` in arch-debt as remaining work. The guard is advisory on preview + pushes and blocking only on `up --prod`. + +## D-IMPL-4 — `deno deploy delete/show` subcommand surface assumption + +- **Severity:** minor (upstream-CLI-shape assumption, isolated). +- The argv builders map `down`→`deno deploy delete` and `status`→`deno deploy show`. The non-`deploy` + subcommand surface of the native `deno deploy` CLI is still stabilizing; the builders are isolated + (`buildDeleteArgs`/`buildStatusArgs`) so a rename is a one-file edit. Verified argv shape by unit + test, not against a live platform account (no credentials in this environment — D5 NEEDS-USER). From 390fbfc9d157a56bdff1d289c0ee140294c020eb Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Fri, 3 Jul 2026 12:28:29 +0200 Subject: [PATCH 12/14] fix(deploy-s6): replace embedded NUL byte with space in unstable-api-guard Eval caveat B1: byte offset 3665 held a literal 0x00 that rendered as a space, marking the whole safety-critical unstable-API guard file as git-binary (Bin 0->4284, invisible to review/IMPL-EVAL) and a JSR/publish landmine. Replaced 0x00 -> 0x20; file is now text and diffable. No behavior change (dedupe key still separates api/source). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012wKHquACkXnWPDgJYhhFjN --- .../domain/deploy/unstable-api-guard.ts | Bin 4284 -> 4284 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts b/packages/cli/src/kernel/domain/deploy/unstable-api-guard.ts index 86caa553fdbe84adc78e2ff66bfd500f11e7a2a9..410e621b71b657393d0e7036cf5937073e141d77 100644 GIT binary patch delta 14 Vcmdm^xJPkAARnW`<{-W-CIBY&1Zn^P delta 14 Vcmdm^xJPkAARi;c<{-W-CIBWi1WEt^ From 015938e1038329a73c657e858b68cd875e696e5c Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Sat, 4 Jul 2026 00:16:44 +0200 Subject: [PATCH 13/14] test(config): assert deno-deploy target retained in drop-behavior test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebase reconciliation: main's #364 drop-behavior test asserted the {windows, linux} target set with `solaris` as the genuinely-unknown throwaway key. Extend it for the deno-deploy member added by this branch — assert `deno-deploy` is retained (org/app) alongside windows+linux and that `solaris` is still dropped. Keeps the union honest post-rebase onto #363+#364. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012wKHquACkXnWPDgJYhhFjN --- packages/config/tests/schema/netscript_config_test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/config/tests/schema/netscript_config_test.ts b/packages/config/tests/schema/netscript_config_test.ts index 3f969ca5d..2e8565087 100644 --- a/packages/config/tests/schema/netscript_config_test.ts +++ b/packages/config/tests/schema/netscript_config_test.ts @@ -115,9 +115,11 @@ Deno.test('defineConfig: drops unknown deploy.targets keys', () => { deploy: { targets: { windows: { mode: 'compile' }, - // `linux` is now a valid member of the target map and is retained. + // `linux` is a valid member of the target map and is retained. linux: { mode: 'compile' }, - // `solaris` is not a member of this slice's target map and is dropped. + // `deno-deploy` is a valid cloud member of the target map and is retained. + 'deno-deploy': { org: 'acme', app: 'orders' }, + // `solaris` is not a member of the target map and is dropped. solaris: { mode: 'compile' }, }, }, @@ -126,7 +128,9 @@ Deno.test('defineConfig: drops unknown deploy.targets keys', () => { assertEquals(config.deploy?.targets?.windows?.mode, 'compile'); assertEquals(config.deploy?.targets?.linux?.mode, 'compile'); - assertEquals(Object.keys(config.deploy?.targets ?? {}), ['windows', 'linux']); + assertEquals(config.deploy?.targets?.['deno-deploy']?.org, 'acme'); + assertEquals(config.deploy?.targets?.['deno-deploy']?.app, 'orders'); + assertEquals(Object.keys(config.deploy?.targets ?? {}), ['windows', 'linux', 'deno-deploy']); }); Deno.test('defineConfig: rejects unrelated saga and trigger section shapes', () => { From 6b9485b6c59f3cf616287c7e9421a30395e3216b Mon Sep 17 00:00:00 2001 From: Rickylabs Date: Sat, 4 Jul 2026 00:52:13 +0200 Subject: [PATCH 14/14] test(deploy): include deno-deploy in default deploy-target registry test Rebase reconciliation: `deploy-target-port_test.ts` pinned the default `DeployTargetRegistry` to exactly the two bare-metal OS-service targets (linux-service, windows-service). This branch registers the tier-1 `deno-deploy` cloud target into the SAME default registry (DEFAULT_DEPLOY_TARGETS in deploy-target-registry.ts), so the sibling assertion regressed on CI check-test (expected [linux-service, windows-service], actual [deno-deploy, linux-service, windows-service]). deno-deploy is a first-class marquee deploy target and legitimately belongs in the default registry (the deno-deploy suite already asserts DEFAULT_DEPLOY_TARGETS registers it). Update the test's expected set to the localeCompare-sorted three keys, rename the test to reflect that it seeds the OS-service targets plus deno-deploy, and assert the deno-deploy label. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012wKHquACkXnWPDgJYhhFjN --- .../kernel/domain/deploy/deploy-target-port_test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/kernel/domain/deploy/deploy-target-port_test.ts b/packages/cli/src/kernel/domain/deploy/deploy-target-port_test.ts index d3fb9575a..6c7937905 100644 --- a/packages/cli/src/kernel/domain/deploy/deploy-target-port_test.ts +++ b/packages/cli/src/kernel/domain/deploy/deploy-target-port_test.ts @@ -96,10 +96,16 @@ Deno.test('a bare-metal service operation resolves a target-scoped descriptor re assertEquals(result.message.includes('/srv/app'), true); }); -Deno.test('the default deploy target registry seeds both bare-metal OS targets', () => { +Deno.test('the default deploy target registry seeds the bare-metal OS targets plus deno-deploy', () => { const registry = new DeployTargetRegistry(); - assertEquals(registry.entries().map(([key]) => key), ['linux-service', 'windows-service']); + // entries() sorts keys with localeCompare for deterministic ordering. + assertEquals(registry.entries().map(([key]) => key), [ + 'deno-deploy', + 'linux-service', + 'windows-service', + ]); assertEquals(registry.get('linux-service')?.label, 'Linux service'); assertEquals(registry.get('windows-service')?.label, 'Windows service'); + assertEquals(registry.get('deno-deploy')?.label, 'Deno Deploy'); });