Skip to content

feat: add --min-age flag and -u shorthand for --update#44

Merged
azu merged 3 commits intoazu:mainfrom
yusuke-koyoshi:feat/add-min-age
Apr 23, 2026
Merged

feat: add --min-age flag and -u shorthand for --update#44
azu merged 3 commits intoazu:mainfrom
yusuke-koyoshi:feat/add-min-age

Conversation

@yusuke-koyoshi
Copy link
Copy Markdown
Contributor

Summary

  • Add --min-age N flag to skip images built within the last N days, acting as a cooldown period for digest pinning
  • Add -u as a shorthand for --update
  • Support min-age in .dockerfile-pin.yaml configuration file

How --min-age works

Uses the image's OCI config Created field to determine build date. When active, uses a combined remote.Image call 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 days
  • Images with no creation timestamp (e.g., reproducible builds) are not skipped

Limitations

  • Tags with specific patch versions (e.g., node:20.11.1) are rarely re-tagged, so --min-age has limited effect on them
  • Most useful for floating tags like mysql:8.0, nginx:latest, ubuntu:24.04 that are periodically updated

Example

# Re-resolve digests, but skip images built within the last 7 days
dockerfile-pin run --write --update --min-age 7
# .dockerfile-pin.yaml
min-age: 7

Test plan

  • go test ./... passes
  • go build . succeeds
  • dockerfile-pin run -u --min-age 7 skips recently built images
  • dockerfile-pin run --min-age 0 works as before
  • dockerfile-pin run --help shows new flags

yusuke-koyoshi and others added 2 commits April 23, 2026 00:56
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>
devin-ai-integration[bot]

This comment was marked as resolved.

@azu azu added the Type: Feature New Feature label Apr 22, 2026
@azu azu requested review from Copilot April 22, 2026 22:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 through runresolveParallel.
  • Add -u shorthand for --update and document it.
  • Add a new resolver helper to fetch digest + image creation time via a single remote.Image call, 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.

Comment thread internal/resolver/resolver.go
Comment thread internal/resolver/resolver.go
Comment thread cmd/pin.go
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>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

if err != nil {
return time.Time{}, fmt.Errorf("reading config for %q: %w", imageRef, err)
}
return cfg.Created.Time, nil
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
return cfg.Created.Time, nil
if cfg.Created == nil {
return time.Time{}, nil
}
return cfg.Created.Time, nil
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner

@azu azu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Thanks!

@azu azu merged commit e659e8f into azu:main Apr 23, 2026
3 checks passed
@github-actions github-actions Bot mentioned this pull request Apr 23, 2026
azu pushed a commit that referenced this pull request Apr 23, 2026
<!-- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Feature New Feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants