diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b73ce4c62..d8077c9a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -301,7 +301,7 @@ jobs: pattern: "commit-boost-*" - name: Sign all binaries with Sigstore - uses: sigstore/gh-action-sigstore-python@v3.0.0 + uses: sigstore/gh-action-sigstore-python@v3.5.0 with: inputs: ./artifacts/**/*.tar.gz diff --git a/bin/tests/binary.rs b/bin/tests/binary.rs index 7036dface..0326d744c 100644 --- a/bin/tests/binary.rs +++ b/bin/tests/binary.rs @@ -130,11 +130,16 @@ fn test_init_compose_file_pbs_service_structure() { assert_eq!(pbs["image"].as_str(), Some("ghcr.io/commit-boost/commit-boost:latest"), "image"); assert_eq!(pbs["container_name"].as_str(), Some("cb_pbs"), "container_name"); - // Config file must be mounted inside the container. + // Config file must be mounted inside the container, from the path it was + // read from. `--config` here is ABSOLUTE, so the source must be that path + // verbatim: a `./` prefix would make compose resolve it against the project + // directory and docker would create a directory there instead. let volumes = pbs["volumes"].as_sequence().expect("volumes is a list"); + let expected_mount = format!("{}:/cb-config.toml:ro", config.display()); assert!( - volumes.iter().any(|v| v.as_str().map_or(false, |s| s.ends_with(":/cb-config.toml:ro"))), - "config must be mounted at /cb-config.toml" + volumes.iter().any(|v| v.as_str() == Some(expected_mount.as_str())), + "config must be mounted at /cb-config.toml from {}, got {volumes:?}", + config.display() ); // Required environment variables must be present. diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index 111ab65e3..8c6737345 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -31,6 +31,19 @@ use docker_compose_types::{ use eyre::Result; use indexmap::IndexMap; +/// A host path as a compose bind-mount source. Compose reads a source with no +/// `/` or `./` prefix as a NAMED VOLUME, so a relative path needs the prefix; +/// an absolute one must be passed through, or it is resolved against the +/// project directory and docker creates a root-owned directory there. +fn compose_bind_source(path: &Path) -> String { + let source = path.display().to_string(); + if path.is_absolute() || source.starts_with("./") || source.starts_with("../") { + source + } else { + format!("./{source}") + } +} + /// Name of the docker compose file pub const CB_COMPOSE_FILE: &str = "cb.docker-compose.yml"; /// Name of the envs file @@ -91,8 +104,8 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re let mut service_config = ServiceCreationInfo { config_info: CommitBoostConfigInfo { config_volume: Volumes::Simple(format!( - "./{}:{}:ro", - config_path.display(), + "{}:{}:ro", + compose_bind_source(&config_path), CONFIG_DEFAULT )), cb_config: CommitBoostConfig::from_file(&config_path)?, @@ -954,6 +967,18 @@ mod tests { // --- get_env_val --- + /// Compose reads a bare source as a named volume, so a relative path keeps + /// the `./` prefix; an absolute one must NOT get it, or compose resolves + /// `.//abs/path` against the project directory and docker creates a + /// root-owned directory there instead of mounting the file. + #[test] + fn test_compose_bind_source_prefixes_only_relative_paths() { + assert_eq!(compose_bind_source(Path::new("cb-config.toml")), "./cb-config.toml"); + assert_eq!(compose_bind_source(Path::new("./cb-config.toml")), "./cb-config.toml"); + assert_eq!(compose_bind_source(Path::new("../cb-config.toml")), "../cb-config.toml"); + assert_eq!(compose_bind_source(Path::new("/etc/cb/config.toml")), "/etc/cb/config.toml"); + } + #[test] fn test_get_env_val_returns_string_pair() { let (key, val) = get_env_val("MY_KEY", "my_value");