refactor: unify workspace root resolution; fix cloud project-config layering + secret-name encoding#167
Closed
markovejnovic wants to merge 3 commits into
Closed
refactor: unify workspace root resolution; fix cloud project-config layering + secret-name encoding#167markovejnovic wants to merge 3 commits into
markovejnovic wants to merge 3 commits into
Conversation
Three verbs each hand-rolled the same root resolution:
let root = match args.dir { Some(d) => d, None => current_dir()? };
in cli/render.rs, cli/pipelines.rs, and commands/run/mod.rs. None of them
consulted a Workspace. Replace all three with Workspace::resolve, which owns
the single rule:
- `--dir` names the project root verbatim, with no walk-up. The backend runs
discovery against cloned repo roots, and walking up out of a clone could
bind to an unrelated `.hm/` above it. A relative `--dir` is resolved against
the cwd, since a workspace root must be absolute.
- No `--dir`: walk up from the cwd, so a verb works from any subdirectory.
Fixes a divergence in `hm run`. render_pipeline took a &RunContext holding a
fully-validated Workspace, ignored it (the parameter was named `_ctx`), and
recomputed the root from `--dir`/cwd. That PathBuf became req.repo_root and
propagated into the scheduler, the archive builder, the secret resolver and
the cloud backend -- so with `--dir`, config came from one tree and code from
another. RunContext::from_cli now takes the verb's `--dir` and resolves once;
render_pipeline uses ctx.workspace.path().
As a side effect `hm run --dir X` now works from a cwd outside any workspace.
It previously failed with "no harmont workspace found" because from_cli walked
up from the cwd and ignored `--dir` entirely.
Behaviour change: `hm render` and `hm pipelines` now walk up when given no
`--dir`, where they were previously cwd-exact and failed in subdirectories.
Discovery is unaffected -- the backend always passes `--dir`.
`hm pipelines` keeps its contract that a repo with no `.hm/` yields the empty
envelope rather than an error. Only NotFound/InvalidPath/InvalidWorkspace map
to it; a malformed `.hm/config.toml` still fails loudly instead of
masquerading as a repo with no pipelines.
CurrentDir and NotFound move onto WorkspaceLoadError, next to the other ways
resolving a root can fail. context::Error is dropped: it had become a
single-variant passthrough.
The cloud verbs called Config::load(None) -- user and env layers only -- so
`[cloud] org` set in a project's .hm/config.toml was silently ignored. The
error message users hit told them to set it in exactly the file being
ignored:
no organization — set `[cloud] org = "…"` in ~/.config/hm/config.toml
(or .hm/config.toml), or run `hm cloud org switch <slug>`
`hm run` read the project layer correctly via ctx.workspace.config(), so the
two halves of the CLI disagreed about the active org in the same directory.
settings::config() now walks up from the cwd and passes the project config
path to Config::load, which already takes an Option<&Path> for precisely
this. Outside a project there is simply no project layer and the cloud verbs
still work from anywhere.
`hm cloud org switch` deliberately keeps Config::load(None): it is a
load-mutate-save on the user file, and merging the project layer in first
would persist project-scoped values into ~/.config/hm/config.toml. Commented
in place so it isn't "fixed" later.
Note a pre-existing wrinkle left alone here: Config::load also merges the env
layer, so `HM_API_URL=x hm cloud org switch y` still writes api_url=x into the
user config.
0d2256d replaced the hand-listed SEGMENT AsciiSet with NON_ALPHANUMERIC, which also escapes `-`, `.`, `_` and `~`. Those are RFC 3986 unreserved and must pass through: `hm cloud secret rm DEPLOY_TOKEN` was requesting /secrets/DEPLOY%5FTOKEN, a different resource than the user named. A name like DEPLOY-TOKEN_v2.0~rc was mangled four ways. Keep that commit's win -- 30 hand-listed bytes are gone -- by deriving the set instead of enumerating it: const SEGMENT: &AsciiSet = &NON_ALPHANUMERIC .remove(b'-').remove(b'.').remove(b'_').remove(b'~'); This is RFC 3986's unreserved set by construction rather than by a list that has to be audited. `/` and space still escape, so a name cannot break out of its path segment. This also unbreaks the test suite, which has been red since 0d2256d. That commit renamed unreserved_name_is_not_encoded to special_chars_are_encoded and flipped its expectation to the escaped output, dropping the comment "RFC 3986 unreserved chars (incl. `-` `_` `.` `~`) must pass through" -- the bug was written down as the spec. It left item_path_appends_encoded_name asserting the opposite, so the two tests contradicted each other and the suite could not pass either way. Restore the original test under its original name, looping over the characters rather than asserting one string -- a single case is what let `_` be noticed while `-.~` broke silently. Add reserved_chars_are_encoded to pin the other half of the contract.
Contributor
Author
|
Superseded by #168, which rebuilds this on #167 was based on |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #152 (
cli-41-secrets) — it builds onhm-core, which only exists there, so--base cli-41-secretskeeps the diff to the refactor alone. Contains no secrets-interface scaffolding.Three independent commits, each builds standalone (verified in detached worktrees).
291b80e— unify root resolution behindWorkspace::resolveThree verbs each hand-rolled the same block, none consulting a
Workspace:Workspace::resolvenow owns the single rule:--dirnames the root verbatim, no walk-up (the backend runs discovery against cloned repo roots — walking up out of a clone could bind to an unrelated.hm/above it); no--dirwalks up from cwd.Fixes a real divergence in
hm run.render_pipelinetook a&RunContextholding a fully-validatedWorkspace, ignored it (the param was literally_ctx), and recomputed the root from--dir/cwd. ThatPathBufbecamereq.repo_rootand propagated into the scheduler, archive builder, secret resolver, and cloud backend — so with--dir, config came from one tree and code from another.Side effect:
hm run --dir Xnow works from a cwd outside any workspace. It previously died with "no harmont workspace found" becausefrom_cliwalked up from cwd and ignored--dir.hm render/hm pipelinesnow walk up when given no--dir, where they were cwd-exact and failed in subdirectories. Discovery is unaffected — the backend always passes--dir.hm pipelineskeeps its empty-envelope contract: onlyNotFound/InvalidPath/InvalidWorkspacemap to it, so a malformed.hm/config.tomlstill fails loudly instead of masquerading as a repo with no pipelines.14c2b2f— cloud verbs read the project.hm/config.tomllayerThey called
Config::load(None)(user + env only), so[cloud] orgin a project config was silently ignored — while the error message named the very file being ignored:hm runread it correctly viactx.workspace.config(), so the two halves of the CLI disagreed about the active org in the same directory.hm cloud org switchdeliberately keepsConfig::load(None)— it's a load-mutate-save on the user file, and merging the project layer would persist project-scoped values into~/.config/hm/config.toml. Commented in place.Pre-existing wrinkle left alone:
Config::loadmerges env, soHM_API_URL=x hm cloud org switch ystill writesapi_url=xto the user config.5520183— stop escaping RFC 3986 unreserved chars in secret names0d2256dswappedSEGMENTforNON_ALPHANUMERIC, which also escapes-._~.hm cloud secret rm DEPLOY_TOKENwas requesting/secrets/DEPLOY%5FTOKEN— a different resource than the user named.DEPLOY-TOKEN_v2.0~rcwas mangled four ways.Keeps that commit's win (30 hand-listed bytes gone) by deriving the set rather than enumerating it:
This also unbreaks the suite, red since
0d2256d. That commit renamedunreserved_name_is_not_encoded→special_chars_are_encodedand flipped its expectation to the escaped output, dropping the comment "RFC 3986 unreserved chars (incl.-_.~) must pass through" — the bug was written down as the spec. It leftitem_path_appends_encoded_nameasserting the opposite, so the two tests contradicted each other and the suite could not pass either way.Verification
206 passed, 0 failed— green for the first time since0d2256d. Beyond tests, the behaviour was driven against the built binary: project-config layering in/outside a project and from nested subdirs;--direxactness (a--dirat a non-workspace subdir returns the empty envelope rather than escaping upward); relative--dir; walk-up from subdirectories; and thehm run --dirdivergence.