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:
- 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.
- 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.
- 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.
- 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`.
Problem
There's no way to rename a project. Users hit this when:
staging-blogbecomesmarketing-site)disco projects:moveexists 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
nameis used as both a stable identifier and a human-facing display label, and those uses leak into a lot of places:project.iddisco-project-{project_id}-{name}{name}-{deployNum}-{service}disco-project-{name}-{deployNum}disco/project-{name}-{image}:{n}/disco/projects/{name},/disco/srv/{name}disco.project.name=(used for lookup via--filter)@iddisco-project-{name}(+-handler-,-hosts-)WorkerTask.project_name(cron worker key)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:utils/caddy.py): switch route@ids fromdisco-project-{name}todisco-project-{id}(plus-handler-and-hosts-variants). Rename helpers to take aProjectinstead of a name string.utils/asyncworker.py): keyWorkerTaskand the cron registry byproject_id.pause_project_crons,remove_project_crons,reload_project_cronsall take an id.utils/docker.py): emit bothdisco.project.id={uuid}anddisco.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 byid. Keep thenamelabel for humans doingdocker ps --filter.disco.project.id, look up the project by their currentdisco.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
Effort
CLI changes for PR 2 land in a parallel PR against `letsdiscodev/cli`.