Skip to content

Add project rename — and fix identifier/display-name conflation that makes it hard #101

Description

@emanuelefaja

Problem

There's no way to rename a project. Users hit this when:

  • They typo'd the project name on creation
  • A project's purpose shifts (e.g. staging-blog becomes marketing-site)
  • They want to standardize naming across an org

disco projects:move exists for cross-server migration but isn't a rename, and using it in-place isn't supported.

Why it isn't a one-line change today

The project's name is used as both a stable identifier and a human-facing display label, and those uses leak into a lot of places:

Surface Format Today
DB FKs (env vars, deployments, github_repo, domains, key_values, command_runs) by project.id ✅ correct
Volumes disco-project-{project_id}-{name} ✅ keyed by id
Docker service name {name}-{deployNum}-{service} display — name is right
Docker network name disco-project-{name}-{deployNum} display — name is right
Image tag disco/project-{name}-{image}:{n} display — name is right
Filesystem /disco/projects/{name}, /disco/srv/{name} display — name is right
Label disco.project.name= (used for lookup via --filter) name ❌ should be id-keyed
Caddy route @id disco-project-{name} (+ -handler-, -hosts-) name ❌ internal, should be id
WorkerTask.project_name (cron worker key) name ❌ internal, should be id

The first group can stay name-based — operators reading docker service ls, docker network ls, or shelling into /disco/projects/... need recognizable names, and UUIDs would make those unreadable.

The second group is internal plumbing. Nobody reads Caddy route @ids by hand, nothing outside the worker depends on the cron key string, and label lookups should be against a stable identifier.

Proposed approach — two PRs

PR 1 — separate identity from display

Move internal references to project.id:

  1. Caddy (utils/caddy.py): switch route @ids from disco-project-{name} to disco-project-{id} (plus -handler- and -hosts- variants). Rename helpers to take a Project instead of a name string.
  2. Async worker (utils/asyncworker.py): key WorkerTask and the cron registry by project_id. pause_project_crons, remove_project_crons, reload_project_crons all take an id.
  3. Docker labels (utils/docker.py): emit both disco.project.id={uuid} and disco.project.name={name} on every service. Switch internal queries (list_services_for_project, list_containers_for_project, list_networks_for_project, list_services_for_deployment) to filter by id. Keep the name label for humans doing docker ps --filter.
  4. Backfill: on daemon start (or as a one-shot Alembic data step), iterate live services lacking disco.project.id, look up the project by their current disco.project.name, and attach the id label. Services not yet backfilled fall back to name-based lookup until their next deploy recreates them.

This PR ships independently. Identity-vs-display separation is a correctness win on its own — no rename feature required.

PR 2 — `PATCH /api/projects/{name}` + CLI

After PR 1, rename collapses to:

```python
async def rename_project(dbsession, project, new_name, by_api_key):
if await get_project_by_name(dbsession, new_name):
raise <422 unique>
await rename_project_dir(project.name, new_name) # /disco/projects/...
await rename_static_deployments_dir_if_any(project.name, new_name)
project.name = new_name
events.project_renamed(...)
```

No Caddy work. No cron work. No service/label rewiring. Live services keep their old display name until next deploy — acceptable, same way stale image tags are.

CLI:

```
disco projects:rename oldname newname [--disco host]
```

Sample flow:

```
$ disco projects:rename my-blog marketing-site
Project renamed to marketing-site.

$ disco projects:list
marketing-site (myorg/blog#main)
```

`PATCH /api/projects/{name}` with body `{"name": "newname"}`. Returns 200 on success, 422 on collision/regex, 404 if source doesn't exist.

Optional polish

After PR 2, we can add an opt-in `--redeploy` flag to `projects:rename` that triggers a fresh deployment so live services pick up the new display name immediately. Not required for correctness.

Risk notes

  • External callers (CI, monitoring) hitting `/api/projects/{old}/...` will 404 post-rename. Document in CLI help and CHANGELOG.
  • Concurrent rename: validate-then-write window is exploitable. PR 1 should also add a unique index on `projects.name` (Alembic revision, with a collision check beforehand).
  • Image cleanup (`utils/imagecleanup.py`) currently queries by name. Update alongside the label refactor in PR 1.

Effort

  • PR 1 (identity/display split): ~1 day, mostly mechanical + tests.
  • PR 2 (rename endpoint + CLI): ~3 hours.

CLI changes for PR 2 land in a parallel PR against `letsdiscodev/cli`.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions