feat: add --min-age flag and -u shorthand for --update#44
Conversation
Add --min-age N flag to skip images built within the last N days, acting as a cooldown period for digest pinning. Uses the image's OCI config Created field to determine build date. Images with no creation timestamp (e.g., reproducible builds) are not skipped. When --min-age is active, uses a combined resolve+age-check via remote.Image to avoid redundant HEAD requests. Also adds -u as a shorthand for --update, and supports setting min-age in the .dockerfile-pin.yaml configuration file. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a “cooldown” mechanism for digest pinning by introducing a --min-age option that can skip recently-built images, plus a -u shorthand for --update, and config support via .dockerfile-pin.yaml.
Changes:
- Add
--min-age N(CLI + config) and plumb it throughrun→resolveParallel. - Add
-ushorthand for--updateand document it. - Add a new resolver helper to fetch digest + image creation time via a single
remote.Imagecall, plus tests for min-age behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/resolver/resolver.go | Adds ResolveWithCreatedTime helper to return digest + OCI created time. |
| cmd/pin.go | Adds --min-age flag, config precedence, and min-age-aware resolving logic. |
| cmd/pin_test.go | Adds tests validating min-age skip/allow behavior and min-age=0 path. |
| cmd/ignore.go | Extends config struct with min-age field. |
| README.md | Documents -u and --min-age usage + config example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
remote.Image resolves to a platform-specific image for multi-arch images, returning a different digest than remote.Head (which returns the manifest list digest). This caused --min-age to pin platform- specific digests instead of portable manifest list digests. Fix by using res.Resolve (HEAD) for digest and a separate GetImageCreatedTime call for the age check. Also proceed with pinning when the age check fails (warn instead of skip). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| if err != nil { | ||
| return time.Time{}, fmt.Errorf("reading config for %q: %w", imageRef, err) | ||
| } | ||
| return cfg.Created.Time, nil |
There was a problem hiding this comment.
🔴 Nil pointer dereference in getImageCreatedTime when image has no creation timestamp
In internal/resolver/resolver.go:145, the code accesses cfg.Created.Time without a nil check. In go-containerregistry v0.20.3, ConfigFile.Created is *v1.Time (a pointer), which is nil when the image config has no created field — this is common for reproducible builds (e.g., images built by ko, bazel, or apko). Accessing .Time on a nil pointer causes a panic/crash.
The function's own doc comment at line 126 says "Returns zero time.Time if the image config has no creation timestamp," and the README at line 386 says "Images with no creation timestamp (e.g., reproducible builds) are not skipped." However, the implementation never reaches a return — it panics first. The tests (TestResolveParallel_MinAge_ZeroCreatedAllowed) only verify this behavior via the mock, never exercising the real getImageCreatedTime.
| return cfg.Created.Time, nil | |
| if cfg.Created == nil { | |
| return time.Time{}, nil | |
| } | |
| return cfg.Created.Time, nil |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
ConfigFile.Created is v1.Time (a value type, not a pointer) in go-containerregistry v0.20.3. It cannot be nil.
https://github.com/google/go-containerregistry/blob/v0.20.3/pkg/v1/config.go#L33
When the created field is absent from the JSON, it deserializes to the zero value of v1.Time, and .Time returns time.Time{} (zero time) safely. No nil check is needed.
<!-- Release notes generated using configuration in .github/release.yml at main --> ## What's Changed ### Features * feat: add --min-age flag and -u shorthand for --update by @yusuke-koyoshi in #44 ## New Contributors * @yusuke-koyoshi made their first contribution in #44 **Full Changelog**: v1.2.2...v1.3.0 Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Summary
--min-age Nflag to skip images built within the last N days, acting as a cooldown period for digest pinning-uas a shorthand for--updatemin-agein.dockerfile-pin.yamlconfiguration fileHow
--min-ageworksUses the image's OCI config
Createdfield to determine build date. When active, uses a combinedremote.Imagecall to resolve both digest and creation time in a single operation (no extra HEAD request).--min-age 0(default): no filtering--min-age 7: skip images built within the last 7 daysLimitations
node:20.11.1) are rarely re-tagged, so--min-agehas limited effect on themmysql:8.0,nginx:latest,ubuntu:24.04that are periodically updatedExample
# Re-resolve digests, but skip images built within the last 7 days dockerfile-pin run --write --update --min-age 7Test plan
go test ./...passesgo build .succeedsdockerfile-pin run -u --min-age 7skips recently built imagesdockerfile-pin run --min-age 0works as beforedockerfile-pin run --helpshows new flags