Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Workspace ids may now be 47 characters instead of 38, so long branch names
keep their tail.** The budget was never a limit anything enforced: devpod's own
ceiling is 48 and fatal (`workspace name cannot be longer than 48 characters` —
a 49-character id is refused, not truncated), and 38 was devlaunch reserving 10
characters for downstream tooling that stacks prefixes onto the container name
against a 64-byte limit. 47 spends nine of those characters on legibility and
keeps one against devpod's wall, leaving ~17 for whatever bolts itself on.
`kinisi_ros@ags-devcontainer-tooling-support` reads
`kinisi-ros-ags-devcontainer-tooling-su-lenevere` rather than
`kinisi-ros-ags-devcontainer-t-lenevere`, and a dependabot ref keeps the whole
action name (`devlaunch-dependabot-codecov-action-6-sifivasa`). Truncation
policy has no effect on collisions — the eight-character suffix is hashed over
the full `(owner, repo, ref)` triple before anything is cut — so nothing about
uniqueness moves with this. Existing containers are unaffected: `dl` addresses
a workspace by the devpod id recorded in `metadata.json` (#88), so an id that
was derived under the old budget keeps opening the same container. A clone
directory whose leaf does change is re-cloned under the new name on the next
launch, leaving the old directory for `dl --prune` to report.

## [0.2.2] - 2026-08-20

### Fixed
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,16 +593,16 @@ them. If that is too often, unset the variable and use `dl <ws> dotfiles`.
see in `dl --ls`) and the clone directory under `~/.cache/devlaunch/repos/`:

```
<repo-slug>-<branch-slug>-<syllables> at most 38 characters
<repo-slug>-<branch-slug>-<syllables> at most 47 characters

blooop/devlaunch@main -> devlaunch-main-zovomobo
blooop/devlaunch@feature/auth -> devlaunch-feature-auth-poliseno
blooop/devlaunch@feature-auth -> devlaunch-feature-auth-nesatabe
blooop/test_renv@nb4 -> test-renv-nb4-polenita
kinisi-robotics/kinisi_ros@ags-devcontainer-tooling-support
-> kinisi-ros-ags-devcontainer-t-lenevere
-> kinisi-ros-ags-devcontainer-tooling-su-lenevere
blooop/devlaunch@dependabot/github_actions/codecov/codecov-action-6
-> devlaunch-dependabot-codecov-sifivasa
-> devlaunch-dependabot-codecov-action-6-sifivasa
```

The eight-character syllable suffix is a hash of the full `(owner, repo, branch)` triple.
Expand All @@ -620,8 +620,11 @@ Branch names are case-sensitive, because git refs are.
URL specs (`dl github.com/owner/repo`) get an id in the same shape, with the suffix
hashed over the URL.

The id is also the container hostname, so it stays well inside the 38-character budget
to leave room for tools that add their own prefixes.
The id is also the container hostname. 47 characters is one inside devpod's own hard
ceiling of 48 — a 49-character id is refused outright, not truncated — and well inside
the 64-byte hostname limit on its own, but tools that stack their own prefixes onto the
container name have about 17 characters to work with, so a tool that wants more is the
one that has to shorten.

Branch names must be safe as both git refs and directory names — a name with a space or
a leading dash is rejected rather than quietly rewritten.
Expand Down
25 changes: 18 additions & 7 deletions devlaunch/workspace_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,23 @@
import re
from dataclasses import dataclass

# Total id budget. devpod's own ceiling is 48, but the setup pass's hostname stage
# Total id budget, one character inside devpod's own ceiling of 48.
#
# 48 is hard and fatal: ``devpod up --id`` with a 49-character id is rejected outright
# (``workspace name cannot be longer than 48 characters``, verified against the pinned
# v0.26.1), so an id derived here that overshoots is a launch that does not happen. 47
# keeps one character of margin from that wall.
#
# This was 38, for the reason it is still not 48: the setup pass's hostname stage
# (``tools.setup_stages``) names the container after the workspace id, and downstream
# tooling stacks prefixes and
# suffixes on it against a 64-byte limit (kinisi-robotics/kinisi_ros#9766 already sat
# at 62/64). 48 is a ceiling others eat into, not a budget to fill, so aim at 38.
TARGET_LENGTH = 38
# tooling stacks its own prefixes and suffixes onto that against a 64-byte limit
# (kinisi-robotics/kinisi_ros#9766 already sat at 62/64 with a 38-char id). Widening to
# 47 spends nine characters of that reserve on legibility — branch names that used to
# lose their tail now keep it — and leaves ~17 characters for whatever a downstream
# caller bolts on, so a caller that wants more is the one that has to shorten. Nothing
# here breaks at any value up to 48, and the id alone is a legal hostname at 47, well
# inside 64.
TARGET_LENGTH = 47

# The repo slug is cut to this length when the id would otherwise overflow, and is
# never cut below it. It carries no identity — only legibility — so trimming it is
Expand All @@ -56,7 +67,7 @@
# This was 3 syllables / 18 bits when the scheme was first decided. 18 bits put a
# birthday collision inside a single repo's plausible branch count — 500 branches
# already produced one in test — so it was widened by one syllable. The extra two
# characters come out of the readable budget; the 38-char cap did not move.
# characters come out of the readable budget; the cap did not move to pay for them.
_CONSONANTS = "bdfghjklmnprstvz"
_VOWELS = "aeio"
_SYLLABLES = 4
Expand Down Expand Up @@ -210,7 +221,7 @@ def value(self) -> str:
suffix = self.suffix
repo_part = slug(self.repo)
# Cut the repo slug only when the id would otherwise overflow. Capping it at
# REPO_SLUG_LENGTH leaves at least TARGET_LENGTH - 20 - 1 - 1 - 8 = 8
# REPO_SLUG_LENGTH leaves at least TARGET_LENGTH - 20 - 1 - 1 - 8 = 17
# characters for the ref, so the ref budget can never go non-positive — the
# hole that let a 47-char repo name skip truncation altogether.
if len(_join(repo_part, _fit_ref(self.ref, TARGET_LENGTH), suffix)) > TARGET_LENGTH:
Expand Down
8 changes: 4 additions & 4 deletions rust/devlaunch-core/src/domain/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,10 +457,10 @@ mod tests {
Case { spec: "blooop/python_template", shape: Shape::OwnerRepo { owner: "blooop", repo: "python_template", branch: None }, expanded: "git@github.com:blooop/python_template.git", identity: Expect::RepoLabel("python-template") },
Case { spec: "blooop/python_template@nb4", shape: Shape::OwnerRepo { owner: "blooop", repo: "python_template", branch: Some("nb4") }, expanded: "git@github.com:blooop/python_template.git@nb4", identity: Expect::Workspace("python-template-nb4-foganeje") },
Case { spec: "loft-sh/devpod", shape: Shape::OwnerRepo { owner: "loft-sh", repo: "devpod", branch: None }, expanded: "git@github.com:loft-sh/devpod.git", identity: Expect::RepoLabel("devpod") },
Case { spec: "owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::OwnerRepo { owner: "owner", repo: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", branch: None }, expanded: "git@github.com:owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.git", identity: Expect::RepoLabel("rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr") },
Case { spec: "owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::OwnerRepo { owner: "owner", repo: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", branch: None }, expanded: "git@github.com:owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.git", identity: Expect::RepoLabel("rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr") },
Case { spec: "owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr@main", shape: Shape::OwnerRepo { owner: "owner", repo: "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", branch: Some("main") }, expanded: "git@github.com:owner/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr.git@main", identity: Expect::Workspace("rrrrrrrrrrrrrrrrrrrr-main-rerajezi") },
Case { spec: "owner/repo@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", shape: Shape::OwnerRepo { owner: "owner", repo: "repo", branch: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }, expanded: "git@github.com:owner/repo.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", identity: Expect::Workspace("repo-aaaaaaaaaaaaaaaaaaaaaaaa-pokipete") },
Case { spec: "owner/myrepo@feature/some-very-long-branch-name-here", shape: Shape::OwnerRepo { owner: "owner", repo: "myrepo", branch: Some("feature/some-very-long-branch-name-here") }, expanded: "git@github.com:owner/myrepo.git@feature/some-very-long-branch-name-here", identity: Expect::Workspace("myrepo-feature-some-very-long-lafazota") },
Case { spec: "owner/repo@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", shape: Shape::OwnerRepo { owner: "owner", repo: "repo", branch: Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") }, expanded: "git@github.com:owner/repo.git@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", identity: Expect::Workspace("repo-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-pokipete") },
Case { spec: "owner/myrepo@feature/some-very-long-branch-name-here", shape: Shape::OwnerRepo { owner: "owner", repo: "myrepo", branch: Some("feature/some-very-long-branch-name-here") }, expanded: "git@github.com:owner/myrepo.git@feature/some-very-long-branch-name-here", identity: Expect::Workspace("myrepo-feature-some-very-long-branch-n-lafazota") },
Case { spec: "user.name/repo.name", shape: Shape::OwnerRepo { owner: "user.name", repo: "repo.name", branch: None }, expanded: "git@github.com:user.name/repo.name.git", identity: Expect::RepoLabel("repo-name") },
Case { spec: "my_user/my_repo", shape: Shape::OwnerRepo { owner: "my_user", repo: "my_repo", branch: None }, expanded: "git@github.com:my_user/my_repo.git", identity: Expect::RepoLabel("my-repo") },
Case { spec: "owner/repo@main", shape: Shape::OwnerRepo { owner: "owner", repo: "repo", branch: Some("main") }, expanded: "git@github.com:owner/repo.git@main", identity: Expect::Workspace("repo-main-gemetato") },
Expand Down Expand Up @@ -491,7 +491,7 @@ mod tests {
Case { spec: "github.com/loft-sh/devpod", shape: Shape::HostPath, expanded: "github.com/loft-sh/devpod", identity: Expect::Workspace("github-com-loft-sh-devpod-vatomiha") },
Case { spec: "github.com/Blooop/DevLaunch", shape: Shape::HostPath, expanded: "github.com/Blooop/DevLaunch", identity: Expect::Workspace("github-com-blooop-devlaunch-lakatoje") },
Case { spec: "github.com/blooop/devlaunch", shape: Shape::HostPath, expanded: "github.com/blooop/devlaunch", identity: Expect::Workspace("github-com-blooop-devlaunch-lakatoje") },
Case { spec: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::HostPath, expanded: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", identity: Expect::Workspace("github-com-oooooooooooooooooo-rasijome") },
Case { spec: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", shape: Shape::HostPath, expanded: "github.com/oooooooooooooooooooooooooooooooooooooooo/rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr", identity: Expect::Workspace("github-com-ooooooooooooooooooooooooooo-rasijome") },
Case { spec: "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/owner/repo", shape: Shape::Url, expanded: "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/owner/repo", identity: Expect::Workspace("github-com-owner-repo-lokolede") },
Case { spec: "/owner/repo@main", shape: Shape::Url, expanded: "/owner/repo@main", identity: Expect::Workspace("github-com-owner-repo-lokolede") },
Case { spec: "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/owner/repo.git", shape: Shape::Url, expanded: "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/owner/repo.git", identity: Expect::Workspace("github-com-owner-repo-lokolede") },
Expand Down
Loading