Skip to content

deployctl 0.14.0 — one console for all your projects - #5

Merged
amal-babu-git merged 5 commits into
mainfrom
feature/projects-console
Oct 6, 2026
Merged

amal-babu-git merged 5 commits into
mainfrom
feature/projects-console

Conversation

@amal-babu-git

Copy link
Copy Markdown
Member

One setup per person (the gh login). Any number of projects on one laptop, each with its own panel, and a switcher between them. Add project handles a new first deployment, a teammate's clone (Import) or a copied-in deployctl. The docs now take a newcomer from nothing to deploying on every merge.

Design brief, with the planning decisions recorded: docs/design/0.14-brief.md.

What changes

1. One setup per person — cli/access.py, deployctl access [show|set-token|forget-token]

  • Which registry login a pull uses, in order:

    1. the environment (CI's own GITHUB_TOKEN);
    2. config/local.env (a per-project override);
    3. ~/.deployctl/credentials.env, a read:packages-only token saved once for every project;
    4. the gh login.

    The last two apply only to ghcr.io. The login is resolved only by commands that pull, never by the status the panel polls.

  • access set-token checks a classic read:packages token with GitHub before saving it (0600). It never takes the token from argv, and refuses fine-grained tokens.

  • The engine: one EXIT/INT/TERM/HUP cleanup logs every host out of the registry and releases the deploy lock. An interrupted or failed run never leaves a login on a host.

  • The panel: Your access reads deployctl access --json. The registry row is optional and never raises the Operate alert. A token can be saved from the card, and a refused ssh key comes with the exact line the owner runs.

2. Many projects, one panel each — cli/projects.py, deployctl projects list|add|remove

  • ~/.deployctl/projects.json holds name, repository and port. Ports are given once from 8766 and kept. Writes are locked and atomic, and a damaged file is refused, never overwritten.
  • deployctl webui:
    • uses the project's own port and reuses a panel already running for it;
    • refuses --port while that panel runs;
    • names whatever else holds the port;
    • new options --detach, --json and --no-browser;
    • outside a project, opens the home panel on 8765.
  • /healthz (token-exempt) says which panel a port serves, and replaces panels/<port>.json.
  • --stop only ever stops a deployctl panel, and waits for the process to exit.
  • paths.find_deploy_dir(repo) finds another project's deploy directory without walking up or reading the caller's DEPLOYCTL_PROJECT.

3. The panel — webui/panel/projects.py; routes.py does not grow

  • Projects ▾ switcher: each project's running tag, read lazily with a read-only deploy status. Opening a project sends only its name; the server looks it up and builds the address.
  • Add project → open / Import / New project (deployctl init --set KEY=VALUE, allowlisted, never secret) / adopt commands.
  • Every dangerous confirmation names the project.
  • An unreachable host now reads hosts unreachable — tag unknown; it used to read nothing deployed yet.

4. Docs

  • README: the three ways in.
  • 05 is now Getting started.
  • New 15-FIRST-DEPLOYMENT: every step, what success looks like, what to do on failure.
  • 50-WEBUI covers projects and the switcher; the command reference covers the new commands; the 10/20/30/35/40 guides follow the access model.
  • CHANGELOG 0.14.0 with upgrade notes; ROADMAP follow-ups. Version 0.14.0.

Hardening found in review:

  • Panels are found even with HTTP_PROXY set.
  • --stop works without lsof.
  • --restart is not refused by a port in TIME_WAIT.

Where this differs from the brief

  • projects.json, not .toml: Python has no TOML writer.
  • Outside a project, webui always opens the home panel, never the last project used.
  • The switcher's running tag is read live, not from a recorded file that goes stale when CI deploys.
  • A copied-in deployctl gets the adopt commands to run in a terminal, not plan/apply in the panel.

Testing

  • uv run pytest -q: 999 passed, up from 833. New:
    • test_access, test_home, test_projects, test_init_values, test_panel_projects;
    • test_webui_launch: real panel processes, including a dead proxy, no lsof, restart;
    • engine tests: logout after a failed run;
    • a wiring check across every panel module.
  • uv run pytest -m e2e: 4 passed. A new Playwright suite, run as its own CI job, drives real panel processes on scratch repositories with TEST-NET hosts and ssh/gh stand-ins:
    • switching projects while a job keeps running;
    • a teammate's clone through Import;
    • a new project to Setup;
    • a confirmation that names the project.
  • shellcheck clean. The built wheel installs on its own, and selftest renders all 6 shapes.
  • By hand, in the browser pane with scratch projects: the Setup tab's access card, the switcher, and the New form pre-filled from origin.

Upgrading

Stop any running panel first. Then install v0.14.0, gh auth refresh -h github.com -s read:packages, deployctl access, deployctl ci init --env production --force. Panels move to per-project ports; 8765 is now the home panel.

Risk to be aware of

With no read:packages token saved, a laptop-driven pull uses the person's gh token. It can also write to their repositories, and a host holds it for the length of the pull (logged out on any exit). This is the default the brief chose. deployctl access and the panel say so, the docs recommend access set-token for production, and ROADMAP records ways to make a narrow login the default.

…oint of use

- cli/home.py: deployctl's own files under the state home — 0700 folders,
  atomic 0600 writes, a cross-process lock.
- cli/access.py: the registry login a pull uses, in order: the environment
  (CI's GITHUB_TOKEN), config/local.env, ~/.deployctl/credentials.env, then
  the gh login. The last two apply only to ghcr.io. Resolved only by
  commands that pull, never by the status the panel polls.
- deployctl access [show|set-token|forget-token]: what you have and what is
  missing, each with its fix. set-token checks a classic read:packages token
  with GitHub before saving it, and never reads it from argv.
- Engine: one EXIT/INT/TERM/HUP cleanup logs every host out of the registry
  and releases the deploy lock, so a failed or cancelled run never leaves a
  login on a host.
- Panel: Your access reads `deployctl access --json`. A missing registry
  login is optional and never raises the Operate alert. The card can save
  a read:packages token, and shows the one line an owner runs to add your
  ssh key.
- Config snapshots are keyed by repository name plus a path id, so two
  repositories with the same name never prune each other's; backup.sh
  honours DEPLOYCTL_HOME.
- Tests: an autouse fixture keeps every test off the developer's own
  credentials.
… its own

- cli/projects.py: ~/.deployctl/projects.json holds name, repository and
  port per project. Ports are given once from 8766 and kept. Writes are
  locked and atomic; a damaged file is refused, never overwritten. A
  copied-in deployctl or a repository without a project says what to do
  instead.
- paths.find_deploy_dir(repo): finds another project's deploy directory
  without walking up or reading the calling project's DEPLOYCTL_PROJECT.
- deployctl projects list|add|remove. init, adopt --apply and webui
  register their project themselves.
- deployctl webui:
  - uses the project's own port;
  - reuses a panel already serving this project, and refuses --port while
    one runs;
  - names whatever else holds the port;
  - --detach (log in ~/.deployctl/logs, returns once it answers), --json,
    and --no-browser (the browser opens by itself only on a desktop,
    never over ssh);
  - outside a project, the home panel on 8765.
- --stop only ever stops a deployctl panel.
- /healthz (no token) says which panel a port serves. It replaces the
  panels/<port>.json records, which named the wrapper's pid and raced a
  failed bind.
- Switcher, top right: every project on this machine with its running tag
  (read-only deploy status, loaded lazily and cached briefly). Choosing one
  opens its own panel, started if need be. Only a name comes from the page,
  and the address is built by the server. The panel you leave keeps running
  with its jobs.
- Add project, from any panel or the home panel:
  - a project opens;
  - a teammate's clone opens on the 0.13 Import;
  - a repository without a project gets the New form;
  - a copied-in deployctl gets the adopt commands, to run in a terminal.
- New project: `deployctl init --set KEY=VALUE`. INIT_KEYS maps each key to
  its file; values are checked before anything is written, are never
  secret, and are refused for a file that already exists. The image is
  suggested from origin, lowercased.
- Every dangerous confirmation names the project, and the page carries it.
- The top bar and the switcher say "hosts unreachable — tag unknown"
  instead of "nothing deployed yet" when a host did not answer.
- webui --stop waits for the process itself, not just the port; panels
  finish in-flight requests within 5s.
- Tests: route tests over HTTP, init --set, and a wiring check across every
  panel module. A Playwright suite (tests/e2e, -m e2e, its own CI job)
  covers switching with a job running, a teammate's clone through Import,
  a new project to Setup, and confirmations, against real panel processes
  with TEST-NET hosts and ssh/gh stand-ins.
- README: one screen on what deployctl is, one install and access step,
  and the three ways in — a new project, joining one, upgrading a
  copied-in one. The layout now shows local.env, exports/, imports/ and
  ~/.deployctl.
- 05-QUICKSTART is now Getting started: install, gh login and access, the
  panel, Add project (new or join), the first deployment, CI/CD, and day
  to day.
- New 15-FIRST-DEPLOYMENT:
  - the checklist (server, DNS, ports, a published image, access);
  - bootstrap, and every configuration field;
  - each first-deploy step: what it does, what success looks like, what
    to do when it fails;
  - SSL, verifying the site, the hand-off to CI/CD, troubleshooting.
- 50-WEBUI: a panel per project, the switcher, the home panel, Add
  project, the safety model's new points, and Your access. The command
  reference, the docs map, and the 10/20/30/35/40 guides follow the new
  access model.
- CHANGELOG 0.14.0 with upgrade notes; ROADMAP follow-ups (a narrower
  default for host pulls, panels left running across an upgrade).
- The 0.14 design brief, with its status corrected and the planning
  decisions recorded.
- Version 0.14.0.
…ter a restart

- /healthz probes never go through HTTP_PROXY. On a network that sets one,
  127.0.0.1 went to the proxy, and a running panel looked like none at
  all, so a second was started on a busy port.
- A port counts as free the way uvicorn binds it, with SO_REUSEADDR. A
  port a panel has just left sits in TIME_WAIT, and `webui --restart` was
  refused as "in use".
- /healthz reports the panel's pid, so `webui --stop` works where lsof is
  not installed. When the holder cannot be named, --stop leaves it alone
  and says so.
- Registering a project again writes nothing when nothing changed, since
  `webui` registers on every start.
@amal-babu-git
amal-babu-git merged commit 41a0e94 into main Oct 6, 2026
5 checks passed
@amal-babu-git
amal-babu-git deleted the feature/projects-console branch October 6, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant