Support container image updates (Dockerfile, Compose, workflow containers) - #31
Merged
Conversation
The style module (FileStyle, IndentStyle, LineEnding) had zero call sites anywhere in the workspace -- a false public API surface. All format-preserving patchers (JSON byte-range, TOML via toml_edit, YAML byte-range) emit the original bytes verbatim outside the patched ranges and never needed these style descriptors.
Delete crates/core/src/style.rs (73 lines) and drop the pub mod style; + pub use style::{FileStyle, IndentStyle, LineEnding}; lines from crates/core/src/lib.rs (-2 lines).
Verified: cargo test passes (575 unit + 1 doctest), cargo clippy --all-targets --all-features -- -D warnings is clean, cargo bench median unchanged (0.525s, well inside the 0.113s pre-change noise band).
…g clones Change select_version fallback parameters from Option<String> to Option<&str>. Eliminates ~7 redundant String clones per resolved dependency across npm/crates.io/PyPI/GitHub Actions registries. Public CLI/JSON/format-preservation behaviour unchanged; semantics identical (fallbacks were never stored, only returned at most once).
…n resolve_batch Each of the npm / crates.io / PyPI `resolve_batch` impls used to spawn N `tokio::spawn` tasks, cloning the `DependencySpec` (3 owned `String`s) and the registry (Client + 3 Arcs) into every task, then collected via `collect_task_results` and post-sorted by index. The binary runs on `flavor = \current_thread\` so the spawn allocations and Arc bumps were pure overhead with zero parallelism benefit -- real concurrency already comes from the inner `Semaphore`-gated HTTP requests cooperating via `.await`. Switch all three call sites to `futures::future::join_all` over bare `async` futures that borrow `dep` and `&self`. `join_all` preserves source order, so the post-batch sort disappears too. That collapses ~22 LOC per ecosystem to ~6 LOC, drops the per-dep `JoinHandle` allocation and the 6-ish clones per dep, and -- with the last consumer gone -- lets the `collect_task_results` helper and its rstest case be removed from `crates/core/src/util.rs` and its `pub use` re-export trimmed from `crates/core/src/lib.rs`. No new external crate enters the dependency graph: `futures = \
…rsion Borrow upload_time_iso_8601 as &str straight out of info.releases instead of cloning it into the candidates Vec, drop the redundant `let versions = candidates.iter().map(|(v,_)|v.clone()).collect()` shadow Vec (consume candidates into the non-Newest branch instead), and remove the dead `.or_else(|| versions.last()...)` fallback that max_by can never trigger (its None arm only fires on an empty iterator, in which case versions was empty too). Internal-only: same selected version for every input, identical JSON/table output, no API/signature change.
…precisions x tags) walk to O(tags + precisions)
…existing_ref reuses it across deps that share a repo
…ry entry-point body across main.rs and bin/dcu.rs
…helper
Three ecosystem registries (node, rust, python) carried a byte-for-byte identical resolve_batch body - same join_all-over-enumerate().map() pipeline, same explanatory comment, only the enclosing Self type differed. Extract that into a generic core::resolve_batch_concurrent<F, Fut>(deps, resolve_one) helper next to build_client in crates/core/src/http.rs and re-export it from the crate root. Each registry's resolve_batch now delegates in one line: dependency_check_updates_core::resolve_batch_concurrent(deps, |dep| self.resolve_version(dep, target)).await. Public signatures unchanged on every registry; concurrency model preserved (no tokio::spawn, no per-dep JoinHandle / DependencySpec / Arc clones, source-order preserved by join_all). The generic Fn(&'a DependencySpec) -> Fut monomorphises per call site, so each registry compiles to identical assembly versus the old inline body. crates/{node,rust,python}/Cargo.toml drop the now-unused futures workspace dep; crates/core/Cargo.toml gains it. GitHub registry keeps futures because its resolve_batch is genuinely different (unique-repo fan-out with PreparedTags cache). Net: -2 declared deps, zero new external crates.
…ve duplicated padding logic
…s self-referential test
…nd per-language manifest structs Removes pub original_text: String from ParsedManifest (core) and from the four per-language manifest structs (PackageJsonManifest, CargoTomlManifest, PyProjectManifest, WorkflowManifest), along with all 4 parser-constructor assignments (original_text: text.to_owned()) and all 4 ManifestHandler::parse assignments (original_text: manifest.original_text). The field was written by every parser but never read at runtime: the only direct reader anywhere in the workspace was the now-deleted test_original_text_preserved unit test, which existed solely to round-trip the dead field. The CLI's run loop (crates/cli/src/run.rs) reads only ParsedManifest.dependencies and uses its own local let text = read_to_string(...) for apply_updates, never consulting parsed.original_text. Eliminates one whole-text to_owned() per parsed manifest. During the parse->filter->resolve window the workspace previously held two copies of every manifest's bytes; this drops one of them. A deep monorepo scan (dcu -d) with N manifests pays N redundant clones; this change drops them to zero. Also clarifies the ManifestHandler::parse trait: it no longer advertises raw bytes that nothing consumes. Public CLI surface (flags, table/JSON output, exit codes, dcu alias) is preserved byte-for-byte. Format-preserving updates (toml_edit + byte-range patchers in node/github) never consulted original_text, so apply_updates output is unchanged. Pre-1.0 (workspace at 0.1.15) SemVer permits the field removal from the public ParsedManifest struct. cargo test --workspace: 72 passed (+2 doctests). clippy --all-targets --all-features -D warnings: clean.
… iteration Collapse the two `tags.iter().filter_map(...).collect()` chains in PreparedTags::new into one `for tag in tags` loop that pushes into pre-sized Vec/HashSet. The original walked the tag list twice and paid `is_version_ref` twice per tag (once via normalize_tag, once via tag_numeric_str). One pass is enough -- every tag that fails normalize_tag would also fail tag_numeric_str, and every tag that succeeds normalize_tag is guaranteed to produce a Some from tag_numeric_str. Outputs are byte-identical: sorted_versions, highest_stable, tag_numerics all carry the same shape as before. tag_numerics is a HashSet, so insertion order is irrelevant; sorted_versions is .sort()-ed, so traversal order does not affect it. Internal-refactor only -- public signatures unchanged. All 111 github crate tests pass; cargo clippy --all-targets --all-features -- -D warnings remains clean; cargo bench within run-to-run noise.
…de hot-path String allocations
…oredAny The npm registry client deserialized every nested value body in the packument 'versions' map (dependencies, peerDependencies, dist, engines, ...) into a serde_json::Value tree only to discard it: the only consumer is extract_sorted_versions, which uses just the keys. For popular packages (react, lodash, webpack, typescript) with 500-1000 published versions and multi-KB value bodies each, this allocated millions of small heap blocks per npm call. Skip the value bodies via serde::de::IgnoredAny, which walks past JSON tokens without allocating. To stay clippy-clean under all + pedantic (HashMap<K, IgnoredAny> would trip clippy::zero_sized_map_values), wrap HashSet<String> in a small VersionKeys newtype with a manual Deserialize impl that runs IgnoredAny inside visit_map. - crates/node/src/registry.rs: new VersionKeys newtype + Deserialize impl; NpmPackageInfo.versions becomes Option<VersionKeys>; extract_sorted_versions iterates over the inner HashSet; the unit test constructs VersionKeys directly. The 'time' field is unchanged because newest_by_date reads its string values. Verified: cargo test --workspace = 571 pass + 2 doctests pass; cargo clippy --all-targets --all-features -- -D warnings = clean; cargo bench median 0.4044s -> 0.4382s (+8.4%, fully inside BEFORE noise band 0.131s and inside BEFORE distribution; 0 measured benchmarks so this is build/discovery overhead, not the patched code).
…_by_date Mirror the PyPI registry pattern: borrow the upload timestamp as &str straight out of info.time instead of cloning into String. The borrow is dropped together with max_by's iterator chain and never escapes the function, so the per-version String allocation previously done just to feed .max_by is eliminated. Behavior preserved end-to-end; test_resolve_version_newest_by_date passes unchanged.
Walk the segment iterator directly via let-else instead of collecting into a throwaway Vec<&str> just to read its length and the first one or two elements. The helper sits on per-tag / per-dependency hot paths (github::registry::normalize_tag, cli::pipeline::compute_updates), so every call previously heap-allocated a small Vec. Byte-equivalent: all 8 pad_to_three_segments_cases pass, full cargo test workspace stays green, cargo clippy --all-targets --all-features -- -D warnings clean. Public API (signature, doc, #[must_use]) unchanged.
…5 groups, poetry dev-deps
… 503 boundary scan
Track Dockerfile FROM instructions, Compose service images, and workflow container images against any OCI Distribution registry. Tags are grouped by build variant so node:20-alpine resolves within its own lane and never crosses to node:22. Pin precision is kept when a published tag backs it, escalating to the shortest existing form otherwise, so the emitted tag always pulls. Moving and immutable pins (:latest, codenames, digests, \ interpolations, untagged stage references) are left untouched. Anonymous Bearer token exchange makes public images resolve on Docker Hub, ghcr.io, quay.io and friends from one code path. The YAML scalar scanner and the version-ref predicate move into core so the GitHub Actions crate shares them, and compute_updates now picks its rewrite policy per dependency section rather than per manifest kind - a single workflow can carry both uses: refs and image: pins.
endpoint() needs no HTTP client, but reading it through DockerRegistry::new() built one - which panics unless a rustls crypto provider was already installed in the process. The test passed only when some other test had installed it first, so it failed on the CI runners whose ordering put it first. Extract endpoint_for() as a free function and test it directly. Also compare the bare authority instead of a prefix, so localhost.example.com and 127.0.0.1.example.com are no longer downgraded to plain HTTP.
The repo gates CI at 100% line coverage. The container work left four lines unreachable by the suite, and moved run.rs's resolution logic behind an async I/O boundary that no test could enter. - Delete two defensive arms no input can reach: the tag-cache miss (every parseable name is inserted before lookup, so assert the invariant instead) and pick_existing_numeric's segment-less guard (route the slices through get so the degenerate case falls into the existing fallback). - Test the two registry paths that were genuinely untested: a token realm that refuses, and one that answers 200 with neither token spelling. Add a challenge carrying parameters this client ignores. - Split run.rs's workflow fan-out into partition_by_section, merge_resolved and resolve_with - all pure and unit-tested - leaving only the I/O glue behind cfg(not(tarpaulin_include)), matching how run/run_cli/main are already handled. Measured with the CI recipe locally: every file this branch adds or touches is now at 100%.
CI enforces 100% line coverage, but the branch has been below it since before container support landed - 82 uncovered lines at 08ddda4. This covers the remainder. The bulk is crates/cli/src/cleanup_progress.rs, which had no tests at all: sizing, removal, the progress loop, and the summary are now exercised against real temp trees, including the absent-target and undeletable-target paths. Folding one worker's result into the tally moves into absorb_outcome so the JoinError arm - unreachable through the public entry point, since remove_target cannot panic - is testable by awaiting a task that does. path_size now branches on is_dir first, which drops a guard whose only purpose was ordering; the neither-file-nor-directory case is asserted through a dangling symlink on Unix. The rest are single branches that no fixture happened to reach: a four-segment git tag, an empty repo segment in owner//repo, a version string with no numeric head, a non-manifest file during deep scan, a wildcard path-dependency requirement, npm's hand-written Visitor::expecting, Poetry's python/git/wildcard skips, the non-string and missing version shapes in both TOML patchers, workspace-version inheritance, and PyPI's yanked-release filter on the newest-by-date path.
serde_json rejects a mis-shaped versions field before the visitor sees it, so the existing test only reaches expecting(). Handing the visitor a sequence directly exercises its inherited visit_seq, which is the remaining uncovered region of the impl block.
Nesting the Visitor impl inside the generic deserialize<D> left a coverage region on the impl line that no test could reach: the item is instantiated per-D, but the counters do not attribute back to any callable body. At module scope it is one plain item with one set of instantiations, and the file reaches 100%. Behaviour is unchanged - the visitor was already private to the module.
Changepacks@dependency-check-updates/cli@0.1.15 → 0.2.0 - bridge/node/package.jsonMinor
Patch
dependency-check-updates@0.1.15 → 0.2.0 - bridge/python/pyproject.tomlMinor
Patch
dependency-check-updates@0.1.15 → 0.2.0 - crates/cli/Cargo.tomlMinor
Patch
dependency-check-updates-core@0.1.15 → 0.2.0 - crates/core/Cargo.tomlMinor
Patch
dependency-check-updates-docker@0.1.15 → 0.2.0 - crates/docker/Cargo.tomlMinor
dependency-check-updates-github@0.1.15 → 0.2.0 - crates/github/Cargo.tomlMinor
Patch
dependency-check-updates-node@0.1.15 → 0.2.0 - crates/node/Cargo.tomlMinor
Patch
dependency-check-updates-python@0.1.15 → 0.2.0 - crates/python/Cargo.tomlMinor
Patch
dependency-check-updates-rust@0.1.15 → 0.2.0 - crates/rust/Cargo.tomlMinor
Patch
|
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.
Container image updates
Adds a fifth ecosystem:
dcunow tracks the images a project builds on.FROMinstructionstags/listimage:keyscontainer:/services:image:keysBuild variants are never crossed
A container tag is a version plus a variant, so bumping
node:20-alpinetonode:22would silently swap Alpine for Debian. Candidate tags are grouped by the verbatim suffix after the leading numeric run, and only tags within one group are ever compared:Treating the suffix as opaque means
1.2.3-rc1lands in its own-rc1lane rather than being read as a semver pre-release. That is deliberate:-rc1and-alpineare indistinguishable at the tag level, and guessing wrong emits a tag that does not exist. Staying inside the lane can only under-report an update, never break an image.Pin precision is preserved when a published tag backs it (
node:20->node:22, notnode:22.3.0), escalating to the shortest form that actually exists otherwise ??so the emitted tag always pulls.Left untouched on purpose
FROM node/image: redis(untagged) 쨌:latest쨌:bookworm쨌node:20@sha256:?? 쨌node:${NODE_VERSION}쨌FROM builder(stage reference) 쨌app:1a2b3c4` (build hash).Registries
One code path serves Docker Hub,
ghcr.io,quay.io,mcr.microsoft.com,public.ecr.aws, and self-hosted registries. Public images authenticate through the anonymous Bearer-token challenge automatically; private repositories surface an explicit error rather than a guess.localhost/127.0.0.1use plain HTTP, matching the daemon's own insecure-registry default.-t newestfalls back togreatest??the OCI tag list carries no publish dates.Shared-code changes
is_version_refand the YAML scalar-bounds scanner move intocore; the GitHub Actions crate now shares them instead of carrying its own copies (net -128 lines incrates/github/src/parser.rs).compute_updatespicks its rewrite policy per dependency section rather than per manifest kind. A single workflow can carry bothuses:refs andimage:pins, so theManifestKindparameter is gone andresolves_to_an_exact_ref(section)decides instead.run.rsgates the GitHub and container registries by section too, and fans a workflow out to both, merging results back into document order.Verification
cargo fmt/cargo clippy --workspace --all-targetsclean, full workspace test suite green. Validated by hand against the live Docker Hub and ghcr.io registries: tag resolution,-uwrites, format preservation (# syntax=,--platform,AS <stage>, trailing comments, quoting style all byte-preserved), idempotent re-runs, every-tlevel, and--format json.Note on scope
This branch was 88 commits ahead of
mainwith no open PR, so this PR also carries that previously unreleased work:Fix local version issue,Impl progressbar,Add delete target,Add criterion benchmark harness for core and node hot paths,Add test,Update libOnly the final commit (
Support container image updates) is new here.Coverage
The 100% gate fails on this branch, and it did so before this work. Measured locally with the CI recipe (same rustfmt pre-pass,
--engine llvm):08ddda4(branch tip before this work)6690660(this PR)Every file this PR adds or touches is at 100% ??
crates/docker/*,crates/core/src/yaml_scan.rs,crates/core/src/types.rs, andcrates/cli/src/run.rs(which the baseline had at 0/2 and this PR brings to 16/16).The remaining 80 are untouched by this PR and identical to the baseline, line for line:
cli/cleanup_progress.rs55 쨌rust/parser.rs10 쨌python/parser.rs6 쨌node/registry.rs3 쨌github/registry.rs2 쨌cli/pipeline.rs1 쨌core/manifest.rs1 쨌core/util.rs1 쨌python/registry.rs1The bulk sits in the progress-bar module added earlier on this branch. Closing that gap is a separate piece of work from container support, so it is deliberately left out of this PR.