docker-devtools 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. docker_devtools-0.0.1/.gitignore +40 -0
  2. docker_devtools-0.0.1/LICENSE +21 -0
  3. docker_devtools-0.0.1/PKG-INFO +227 -0
  4. docker_devtools-0.0.1/README.md +199 -0
  5. docker_devtools-0.0.1/cmd/docker-devtools/context.go +166 -0
  6. docker_devtools-0.0.1/cmd/docker-devtools/image.go +105 -0
  7. docker_devtools-0.0.1/cmd/docker-devtools/main.go +119 -0
  8. docker_devtools-0.0.1/go.mod +37 -0
  9. docker_devtools-0.0.1/go.sum +76 -0
  10. docker_devtools-0.0.1/hatch_build.py +145 -0
  11. docker_devtools-0.0.1/internal/imgref/compose.go +89 -0
  12. docker_devtools-0.0.1/internal/imgref/dockerfile.go +119 -0
  13. docker_devtools-0.0.1/internal/imgref/imgref.go +121 -0
  14. docker_devtools-0.0.1/internal/imgref/imgref_test.go +146 -0
  15. docker_devtools-0.0.1/internal/imgref/scan.go +116 -0
  16. docker_devtools-0.0.1/internal/imgupdate/tagpolicy.go +153 -0
  17. docker_devtools-0.0.1/internal/imgupdate/tagpolicy_test.go +103 -0
  18. docker_devtools-0.0.1/internal/imgupdate/update.go +218 -0
  19. docker_devtools-0.0.1/internal/imgupdate/update_test.go +192 -0
  20. docker_devtools-0.0.1/internal/plugin/plugin.go +204 -0
  21. docker_devtools-0.0.1/internal/plugin/plugin_test.go +126 -0
  22. docker_devtools-0.0.1/internal/registry/registry.go +68 -0
  23. docker_devtools-0.0.1/internal/rewrite/rewrite.go +60 -0
  24. docker_devtools-0.0.1/internal/rewrite/rewrite_test.go +98 -0
  25. docker_devtools-0.0.1/pyproject.toml +249 -0
  26. docker_devtools-0.0.1/scripts/build-wheels.sh +40 -0
  27. docker_devtools-0.0.1/scripts/commit-msg-lint.sh +24 -0
  28. docker_devtools-0.0.1/scripts/completions.sh +43 -0
  29. docker_devtools-0.0.1/scripts/conformance.sh +66 -0
  30. docker_devtools-0.0.1/scripts/prose-lint.sh +33 -0
  31. docker_devtools-0.0.1/scripts/test-fresh-clone.sh +95 -0
  32. docker_devtools-0.0.1/src/docker_devtools/__init__.py +198 -0
  33. docker_devtools-0.0.1/src/docker_devtools/__main__.py +33 -0
  34. docker_devtools-0.0.1/src/docker_devtools/_find.py +116 -0
  35. docker_devtools-0.0.1/src/docker_devtools/_version.py +24 -0
  36. docker_devtools-0.0.1/src/docker_devtools/py.typed +0 -0
  37. docker_devtools-0.0.1/testdata/dctx/lowercase/all.golden +9 -0
  38. docker_devtools-0.0.1/testdata/dctx/lowercase/context/dockerfile +2 -0
  39. docker_devtools-0.0.1/testdata/dctx/lowercase/context/dockerfile.dockerignore +1 -0
  40. docker_devtools-0.0.1/testdata/dctx/lowercase/context/keep +1 -0
  41. docker_devtools-0.0.1/testdata/dctx/lowercase/context/secret +1 -0
  42. docker_devtools-0.0.1/testdata/dctx/lowercase/ignored.golden +6 -0
  43. docker_devtools-0.0.1/testdata/dctx/lowercase/included.golden +8 -0
  44. docker_devtools-0.0.1/testdata/dctx/none/all.golden +8 -0
  45. docker_devtools-0.0.1/testdata/dctx/none/context/Dockerfile +2 -0
  46. docker_devtools-0.0.1/testdata/dctx/none/context/a.txt +1 -0
  47. docker_devtools-0.0.1/testdata/dctx/none/context/b.txt +1 -0
  48. docker_devtools-0.0.1/testdata/dctx/none/ignored.golden +5 -0
  49. docker_devtools-0.0.1/testdata/dctx/none/included.golden +8 -0
  50. docker_devtools-0.0.1/testdata/dctx/order/all.golden +12 -0
  51. docker_devtools-0.0.1/testdata/dctx/order/context/.dockerignore +2 -0
  52. docker_devtools-0.0.1/testdata/dctx/order/context/CHANGELOG.md +1 -0
  53. docker_devtools-0.0.1/testdata/dctx/order/context/Dockerfile +2 -0
  54. docker_devtools-0.0.1/testdata/dctx/order/context/README.md +1 -0
  55. docker_devtools-0.0.1/testdata/dctx/order/context/docs/guide.md +1 -0
  56. docker_devtools-0.0.1/testdata/dctx/order/context/main.go +1 -0
  57. docker_devtools-0.0.1/testdata/dctx/order/ignored.golden +6 -0
  58. docker_devtools-0.0.1/testdata/dctx/order/included.golden +11 -0
  59. docker_devtools-0.0.1/testdata/dctx/percent/all.golden +9 -0
  60. docker_devtools-0.0.1/testdata/dctx/percent/case.json +1 -0
  61. docker_devtools-0.0.1/testdata/dctx/percent/context/nope +1 -0
  62. docker_devtools-0.0.1/testdata/dctx/percent/context/we%ird +2 -0
  63. docker_devtools-0.0.1/testdata/dctx/percent/context/we%ird.dockerignore +1 -0
  64. docker_devtools-0.0.1/testdata/dctx/percent/context/yes +1 -0
  65. docker_devtools-0.0.1/testdata/dctx/percent/ignored.golden +6 -0
  66. docker_devtools-0.0.1/testdata/dctx/percent/included.golden +8 -0
  67. docker_devtools-0.0.1/testdata/dctx/perdockerfile/all.golden +11 -0
  68. docker_devtools-0.0.1/testdata/dctx/perdockerfile/case.json +1 -0
  69. docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/.dockerignore +1 -0
  70. docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/Prj1 +2 -0
  71. docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/Prj1.dockerignore +1 -0
  72. docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/only-default +1 -0
  73. docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/only-prj1 +1 -0
  74. docker_devtools-0.0.1/testdata/dctx/perdockerfile/context/other +1 -0
  75. docker_devtools-0.0.1/testdata/dctx/perdockerfile/ignored.golden +6 -0
  76. docker_devtools-0.0.1/testdata/dctx/perdockerfile/included.golden +10 -0
  77. docker_devtools-0.0.1/testdata/dctx/pycache/all.golden +18 -0
  78. docker_devtools-0.0.1/testdata/dctx/pycache/context/.dockerignore +1 -0
  79. docker_devtools-0.0.1/testdata/dctx/pycache/context/Dockerfile +2 -0
  80. docker_devtools-0.0.1/testdata/dctx/pycache/context/a/b/keep.py +1 -0
  81. docker_devtools-0.0.1/testdata/dctx/pycache/context/main.py +1 -0
  82. docker_devtools-0.0.1/testdata/dctx/pycache/context/pkg/mod.py +1 -0
  83. docker_devtools-0.0.1/testdata/dctx/pycache/ignored.golden +10 -0
  84. docker_devtools-0.0.1/testdata/dctx/pycache/included.golden +13 -0
  85. docker_devtools-0.0.1/testdata/dctx/reinclude/all.golden +13 -0
  86. docker_devtools-0.0.1/testdata/dctx/reinclude/context/.dockerignore +2 -0
  87. docker_devtools-0.0.1/testdata/dctx/reinclude/context/Dockerfile +2 -0
  88. docker_devtools-0.0.1/testdata/dctx/reinclude/context/app.js +1 -0
  89. docker_devtools-0.0.1/testdata/dctx/reinclude/context/node_modules/drop/index.js +1 -0
  90. docker_devtools-0.0.1/testdata/dctx/reinclude/context/node_modules/keep/index.js +1 -0
  91. docker_devtools-0.0.1/testdata/dctx/reinclude/ignored.golden +7 -0
  92. docker_devtools-0.0.1/testdata/dctx/reinclude/included.golden +11 -0
  93. docker_devtools-0.0.1/testdata/dctx/trailing/all.golden +12 -0
  94. docker_devtools-0.0.1/testdata/dctx/trailing/context/.dockerignore +1 -0
  95. docker_devtools-0.0.1/testdata/dctx/trailing/context/Dockerfile +2 -0
  96. docker_devtools-0.0.1/testdata/dctx/trailing/context/build/out.bin +1 -0
  97. docker_devtools-0.0.1/testdata/dctx/trailing/context/build/sub/deep.bin +1 -0
  98. docker_devtools-0.0.1/testdata/dctx/trailing/context/src.go +1 -0
  99. docker_devtools-0.0.1/testdata/dctx/trailing/ignored.golden +9 -0
  100. docker_devtools-0.0.1/testdata/dctx/trailing/included.golden +8 -0
  101. docker_devtools-0.0.1/testdata/imageref/compose/compose.yaml +25 -0
  102. docker_devtools-0.0.1/testdata/imageref/compose/expected.txt +4 -0
  103. docker_devtools-0.0.1/testdata/imageref/edge/Dockerfile +2 -0
  104. docker_devtools-0.0.1/testdata/imageref/edge/Dockerfile.pinned +2 -0
  105. docker_devtools-0.0.1/testdata/imageref/edge/expected.txt +2 -0
  106. docker_devtools-0.0.1/testdata/imageref/multistage/Dockerfile +17 -0
  107. docker_devtools-0.0.1/testdata/imageref/multistage/expected.txt +7 -0
  108. docker_devtools-0.0.1/tests/__init__.py +0 -0
  109. docker_devtools-0.0.1/tests/api_test.py +63 -0
  110. docker_devtools-0.0.1/tests/binary_test.py +54 -0
  111. docker_devtools-0.0.1/tests/conftest.py +67 -0
@@ -0,0 +1,40 @@
1
+ # Go
2
+ /build/
3
+ *.test
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ .venv/
9
+ venv/
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .mypy_cache/
16
+ .coverage
17
+ .coverage.*
18
+ coverage.xml
19
+ htmlcov/
20
+
21
+ # hatch-vcs writes this at build time
22
+ _version.py
23
+
24
+ # uv
25
+ uv.lock
26
+ requirements.txt
27
+
28
+ # vale style packs are fetched by `vale sync`
29
+ .vale-styles/
30
+ .vale-commits/styles/
31
+ .vale-commits/msg.md
32
+
33
+ .DS_Store
34
+
35
+ # The fixture contexts are the data under test. They deliberately contain
36
+ # directories the rules above ignore (__pycache__, build/), because ignoring
37
+ # those is exactly the behaviour being checked. Re-include everything under
38
+ # testdata; the negation has to name the directories too, since git will not
39
+ # descend into an excluded one.
40
+ !/testdata/**
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flavio Amurrio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.5
2
+ Name: docker-devtools
3
+ Version: 0.0.1
4
+ Summary: Work on the Dockerfiles, Compose files and build context in a repository.
5
+ Project-URL: Documentation, https://github.com/FlavioAmurrioCS/docker-devtools#readme
6
+ Project-URL: Issues, https://github.com/FlavioAmurrioCS/docker-devtools/issues
7
+ Project-URL: Source, https://github.com/FlavioAmurrioCS/docker-devtools
8
+ Author-email: Flavio Amurrio <25621374+FlavioAmurrioCS@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: build-context,buildkit,docker,docker-compose,dockerfile,dockerignore,pre-commit
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Go
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
24
+ Classifier: Topic :: Software Development :: Build Tools
25
+ Classifier: Topic :: Utilities
26
+ Requires-Python: >=3.10
27
+ Description-Content-Type: text/markdown
28
+
29
+ # docker-devtools
30
+
31
+ Work on the Docker files in a repository: the build context a Dockerfile would
32
+ send, and the image references it and your Compose files point at.
33
+
34
+ ```console
35
+ $ docker-devtools image ls
36
+ Dockerfile:1 python:3.11-slim
37
+ compose.yaml:3 nginx:1.25-alpine
38
+
39
+ $ docker-devtools image update --tag-policy same-pattern --dry-run
40
+ Dockerfile:1 python:3.11-slim -> python:3.14-slim (tag 3.11-slim -> 3.14-slim)
41
+ compose.yaml:3 nginx:1.25-alpine -> nginx:1.31-alpine (tag 1.25-alpine -> 1.31-alpine)
42
+ ```
43
+
44
+ ## Why another one
45
+
46
+ Renovate and Dependabot already update image references, and they do it well.
47
+ They run as bots against a repository and open pull requests. This one runs on
48
+ your machine and edits the files in place. It is fast enough for a pre-commit
49
+ hook, so a stale base image gets caught before it is ever committed.
50
+
51
+ Where the semantics are Docker's, this defers to Docker's own code:
52
+
53
+ | Step | Package |
54
+ | --- | --- |
55
+ | Parse Dockerfiles | `moby/buildkit/frontend/dockerfile/parser` and `instructions` |
56
+ | Parse image references | `google/go-containerregistry/pkg/name` |
57
+ | Talk to registries | `google/go-containerregistry/pkg/v1/remote` |
58
+ | Match .dockerignore rules | `moby/patternmatcher` |
59
+ | Walk a build context | `tonistiigi/fsutil`, the package BuildKit sends contexts with |
60
+
61
+ None of the `.dockerignore` semantics are reimplemented here, and CI checks
62
+ that rather than asserting it: for every fixture, `scripts/conformance.sh`
63
+ builds `FROM scratch` with `COPY . /`, exports the image as a tarball, and
64
+ diffs the tar members against what `context ls` reports.
65
+
66
+ ## Install
67
+
68
+ ```console
69
+ $ uvx docker-devtools image ls # no install
70
+ $ pipx run docker-devtools image ls # no install
71
+ $ uv tool install docker-devtools
72
+ $ pip install docker-devtools
73
+ $ mise use ubi:FlavioAmurrioCS/docker-devtools
74
+ ```
75
+
76
+ Prebuilt binaries are attached to each
77
+ [release](https://github.com/FlavioAmurrioCS/docker-devtools/releases). With a
78
+ Go toolchain:
79
+
80
+ ```console
81
+ $ go install github.com/FlavioAmurrioCS/docker-devtools/cmd/docker-devtools@latest
82
+ ```
83
+
84
+ Reading and updating files doesn't require a Docker installation or a running
85
+ daemon.
86
+ Registry lookups authenticate with the same `~/.docker/config.json` the docker
87
+ CLI uses.
88
+
89
+ ## Usage
90
+
91
+ ```text
92
+ docker-devtools context ls [PATH] list the files Docker would send
93
+ docker-devtools context explain PATH show which .dockerignore rule decided a path
94
+ docker-devtools image ls [PATH...] list every image reference, with file and line
95
+ docker-devtools image update [PATH...] rewrite references in place
96
+ docker-devtools install-docker-plugin register as "docker devtools"
97
+ ```
98
+
99
+ ### Updating image references
100
+
101
+ What changes is split by how much judgement it needs.
102
+
103
+ `--pin-digest` resolves the current tag to a digest and appends it, turning
104
+ `nginx:1.29` into `nginx:1.29@sha256:…`. It doesn't decide anything about versions, so it is
105
+ reversible and safe to run anywhere.
106
+
107
+ `--tag-policy` moves the tag. The default, `same-pattern`, moves only the last
108
+ component and keeps the suffix, so how specific your tag is decides how far it
109
+ may move:
110
+
111
+ | Current tag | same-pattern | minor | patch | latest |
112
+ | --- | --- | --- | --- | --- |
113
+ | `3.12-slim` | `3.13-slim` | `3.13-slim` | no change | `4.0-slim` |
114
+ | `3.12.1-slim` | `3.12.7-slim` | `3.13.0-slim` | `3.12.7-slim` | `4.0-slim` |
115
+ | `latest` | no change | no change | no change | no change |
116
+
117
+ No policy ever changes the suffix: `-alpine` and `-slim` are different images,
118
+ and swapping them would change your base distribution without saying so. Tags
119
+ with no version, such as `latest` or `bookworm`, are never moved, because there
120
+ is no ordering to move along.
121
+
122
+ Add `--dry-run` to see the plan without writing, and `--fail-on-diff` to exit
123
+ non-zero when anything would change, which is what makes it useful in CI.
124
+
125
+ ### What it will not touch
126
+
127
+ Some references cannot be resolved to an image, and those are reported rather
128
+ than guessed at. Pass `--unresolved` to `image ls` to see them:
129
+
130
+ - `FROM builder`, where `builder` is an earlier stage
131
+ - `COPY --from=0`, which indexes a stage
132
+ - `FROM $BASE`, which depends on a build argument
133
+ - `FROM scratch`, which is the empty base rather than a registry image
134
+ - Compose values built from variables, such as `${REGISTRY}/app:latest`
135
+
136
+ ### Editing in place
137
+
138
+ An update splices the new reference into the exact byte range the parser
139
+ reported. It never re-encodes the file, so comments, quoting style, anchors and
140
+ whitespace all survive:
141
+
142
+ ```yaml
143
+ image: "nginx:1.29-alpine" # keep this comment and the quotes
144
+ ```
145
+
146
+ becomes
147
+
148
+ ```yaml
149
+ image: "nginx:1.31-alpine" # keep this comment and the quotes
150
+ ```
151
+
152
+ If a byte range no longer holds the text the parse said it held, the update
153
+ fails instead of writing. A rewrite that has drifted from the parse is a bug,
154
+ and corrupting the file would hide it.
155
+
156
+ ## Shell completion
157
+
158
+ The binary emits a [usage](https://usage.jdx.dev) spec describing its own
159
+ command tree, and the `usage` CLI turns that into completions for bash, zsh,
160
+ fish, powershell and nushell:
161
+
162
+ ```console
163
+ $ mise use usage
164
+ $ usage g completion zsh docker-devtools --usage-cmd 'docker-devtools --usage-spec' --install
165
+ ```
166
+
167
+ The generated scripts call back to `usage` at completion time, so it has to stay
168
+ on your PATH. `mise run completions` regenerates all five, plus a markdown
169
+ reference, into `build/`.
170
+
171
+ ## As a Docker CLI plugin
172
+
173
+ ```console
174
+ $ docker-devtools install-docker-plugin
175
+ $ docker devtools image ls
176
+ ```
177
+
178
+ This symlinks the binary into `~/.docker/cli-plugins/`. Use `--system` to
179
+ install it for every user.
180
+
181
+ The subcommand is `devtools` because Docker validates plugin names against
182
+ `^[a-z][a-z0-9]*$` and refuses to load anything else. Python wheels cannot do
183
+ this step at install time: they have no post-install hook, and
184
+ `~/.docker/cli-plugins/` sits outside every Python install path.
185
+
186
+ ## Python API
187
+
188
+ The wheel bundles the binary and a typed wrapper.
189
+
190
+ ```python
191
+ from docker_devtools import image_ls
192
+ from docker_devtools import image_update
193
+
194
+ for ref in image_ls(".").resolved():
195
+ print(f"{ref.path}:{ref.line}", ref.repository, ref.tag)
196
+
197
+ report = image_update(".", pin_digest=True, dry_run=True)
198
+ for change in report.changes:
199
+ print(change.old, "->", change.new, f"({change.reason})")
200
+ ```
201
+
202
+ `image_update` defaults to `dry_run=True`, so calling it by accident cannot
203
+ rewrite a repository.
204
+
205
+ ## Development
206
+
207
+ `mise.toml` defines the tools and the tasks.
208
+
209
+ ```console
210
+ $ mise run build # compile into ./build
211
+ $ mise run test # go test + pytest
212
+ $ mise run lint # pre-commit across the repo
213
+ $ mise run conformance # diff context listing against real docker build
214
+ $ mise run completions # regenerate completions and docs
215
+ $ mise run wheels # every platform wheel into ./dist
216
+ $ mise run test-clone # verify a fresh clone in a container
217
+ ```
218
+
219
+ Registry behaviour is tested against `go-containerregistry`'s in-process
220
+ registry, so the suite doesn't touch the network or carry recorded fixtures.
221
+
222
+ ## License
223
+
224
+ MIT. See [LICENSE](LICENSE).
225
+
226
+ `src/docker_devtools/_find.py` adapts the binary-discovery search order from
227
+ [uv](https://github.com/astral-sh/uv), which is MIT OR Apache-2.0.
@@ -0,0 +1,199 @@
1
+ # docker-devtools
2
+
3
+ Work on the Docker files in a repository: the build context a Dockerfile would
4
+ send, and the image references it and your Compose files point at.
5
+
6
+ ```console
7
+ $ docker-devtools image ls
8
+ Dockerfile:1 python:3.11-slim
9
+ compose.yaml:3 nginx:1.25-alpine
10
+
11
+ $ docker-devtools image update --tag-policy same-pattern --dry-run
12
+ Dockerfile:1 python:3.11-slim -> python:3.14-slim (tag 3.11-slim -> 3.14-slim)
13
+ compose.yaml:3 nginx:1.25-alpine -> nginx:1.31-alpine (tag 1.25-alpine -> 1.31-alpine)
14
+ ```
15
+
16
+ ## Why another one
17
+
18
+ Renovate and Dependabot already update image references, and they do it well.
19
+ They run as bots against a repository and open pull requests. This one runs on
20
+ your machine and edits the files in place. It is fast enough for a pre-commit
21
+ hook, so a stale base image gets caught before it is ever committed.
22
+
23
+ Where the semantics are Docker's, this defers to Docker's own code:
24
+
25
+ | Step | Package |
26
+ | --- | --- |
27
+ | Parse Dockerfiles | `moby/buildkit/frontend/dockerfile/parser` and `instructions` |
28
+ | Parse image references | `google/go-containerregistry/pkg/name` |
29
+ | Talk to registries | `google/go-containerregistry/pkg/v1/remote` |
30
+ | Match .dockerignore rules | `moby/patternmatcher` |
31
+ | Walk a build context | `tonistiigi/fsutil`, the package BuildKit sends contexts with |
32
+
33
+ None of the `.dockerignore` semantics are reimplemented here, and CI checks
34
+ that rather than asserting it: for every fixture, `scripts/conformance.sh`
35
+ builds `FROM scratch` with `COPY . /`, exports the image as a tarball, and
36
+ diffs the tar members against what `context ls` reports.
37
+
38
+ ## Install
39
+
40
+ ```console
41
+ $ uvx docker-devtools image ls # no install
42
+ $ pipx run docker-devtools image ls # no install
43
+ $ uv tool install docker-devtools
44
+ $ pip install docker-devtools
45
+ $ mise use ubi:FlavioAmurrioCS/docker-devtools
46
+ ```
47
+
48
+ Prebuilt binaries are attached to each
49
+ [release](https://github.com/FlavioAmurrioCS/docker-devtools/releases). With a
50
+ Go toolchain:
51
+
52
+ ```console
53
+ $ go install github.com/FlavioAmurrioCS/docker-devtools/cmd/docker-devtools@latest
54
+ ```
55
+
56
+ Reading and updating files doesn't require a Docker installation or a running
57
+ daemon.
58
+ Registry lookups authenticate with the same `~/.docker/config.json` the docker
59
+ CLI uses.
60
+
61
+ ## Usage
62
+
63
+ ```text
64
+ docker-devtools context ls [PATH] list the files Docker would send
65
+ docker-devtools context explain PATH show which .dockerignore rule decided a path
66
+ docker-devtools image ls [PATH...] list every image reference, with file and line
67
+ docker-devtools image update [PATH...] rewrite references in place
68
+ docker-devtools install-docker-plugin register as "docker devtools"
69
+ ```
70
+
71
+ ### Updating image references
72
+
73
+ What changes is split by how much judgement it needs.
74
+
75
+ `--pin-digest` resolves the current tag to a digest and appends it, turning
76
+ `nginx:1.29` into `nginx:1.29@sha256:…`. It doesn't decide anything about versions, so it is
77
+ reversible and safe to run anywhere.
78
+
79
+ `--tag-policy` moves the tag. The default, `same-pattern`, moves only the last
80
+ component and keeps the suffix, so how specific your tag is decides how far it
81
+ may move:
82
+
83
+ | Current tag | same-pattern | minor | patch | latest |
84
+ | --- | --- | --- | --- | --- |
85
+ | `3.12-slim` | `3.13-slim` | `3.13-slim` | no change | `4.0-slim` |
86
+ | `3.12.1-slim` | `3.12.7-slim` | `3.13.0-slim` | `3.12.7-slim` | `4.0-slim` |
87
+ | `latest` | no change | no change | no change | no change |
88
+
89
+ No policy ever changes the suffix: `-alpine` and `-slim` are different images,
90
+ and swapping them would change your base distribution without saying so. Tags
91
+ with no version, such as `latest` or `bookworm`, are never moved, because there
92
+ is no ordering to move along.
93
+
94
+ Add `--dry-run` to see the plan without writing, and `--fail-on-diff` to exit
95
+ non-zero when anything would change, which is what makes it useful in CI.
96
+
97
+ ### What it will not touch
98
+
99
+ Some references cannot be resolved to an image, and those are reported rather
100
+ than guessed at. Pass `--unresolved` to `image ls` to see them:
101
+
102
+ - `FROM builder`, where `builder` is an earlier stage
103
+ - `COPY --from=0`, which indexes a stage
104
+ - `FROM $BASE`, which depends on a build argument
105
+ - `FROM scratch`, which is the empty base rather than a registry image
106
+ - Compose values built from variables, such as `${REGISTRY}/app:latest`
107
+
108
+ ### Editing in place
109
+
110
+ An update splices the new reference into the exact byte range the parser
111
+ reported. It never re-encodes the file, so comments, quoting style, anchors and
112
+ whitespace all survive:
113
+
114
+ ```yaml
115
+ image: "nginx:1.29-alpine" # keep this comment and the quotes
116
+ ```
117
+
118
+ becomes
119
+
120
+ ```yaml
121
+ image: "nginx:1.31-alpine" # keep this comment and the quotes
122
+ ```
123
+
124
+ If a byte range no longer holds the text the parse said it held, the update
125
+ fails instead of writing. A rewrite that has drifted from the parse is a bug,
126
+ and corrupting the file would hide it.
127
+
128
+ ## Shell completion
129
+
130
+ The binary emits a [usage](https://usage.jdx.dev) spec describing its own
131
+ command tree, and the `usage` CLI turns that into completions for bash, zsh,
132
+ fish, powershell and nushell:
133
+
134
+ ```console
135
+ $ mise use usage
136
+ $ usage g completion zsh docker-devtools --usage-cmd 'docker-devtools --usage-spec' --install
137
+ ```
138
+
139
+ The generated scripts call back to `usage` at completion time, so it has to stay
140
+ on your PATH. `mise run completions` regenerates all five, plus a markdown
141
+ reference, into `build/`.
142
+
143
+ ## As a Docker CLI plugin
144
+
145
+ ```console
146
+ $ docker-devtools install-docker-plugin
147
+ $ docker devtools image ls
148
+ ```
149
+
150
+ This symlinks the binary into `~/.docker/cli-plugins/`. Use `--system` to
151
+ install it for every user.
152
+
153
+ The subcommand is `devtools` because Docker validates plugin names against
154
+ `^[a-z][a-z0-9]*$` and refuses to load anything else. Python wheels cannot do
155
+ this step at install time: they have no post-install hook, and
156
+ `~/.docker/cli-plugins/` sits outside every Python install path.
157
+
158
+ ## Python API
159
+
160
+ The wheel bundles the binary and a typed wrapper.
161
+
162
+ ```python
163
+ from docker_devtools import image_ls
164
+ from docker_devtools import image_update
165
+
166
+ for ref in image_ls(".").resolved():
167
+ print(f"{ref.path}:{ref.line}", ref.repository, ref.tag)
168
+
169
+ report = image_update(".", pin_digest=True, dry_run=True)
170
+ for change in report.changes:
171
+ print(change.old, "->", change.new, f"({change.reason})")
172
+ ```
173
+
174
+ `image_update` defaults to `dry_run=True`, so calling it by accident cannot
175
+ rewrite a repository.
176
+
177
+ ## Development
178
+
179
+ `mise.toml` defines the tools and the tasks.
180
+
181
+ ```console
182
+ $ mise run build # compile into ./build
183
+ $ mise run test # go test + pytest
184
+ $ mise run lint # pre-commit across the repo
185
+ $ mise run conformance # diff context listing against real docker build
186
+ $ mise run completions # regenerate completions and docs
187
+ $ mise run wheels # every platform wheel into ./dist
188
+ $ mise run test-clone # verify a fresh clone in a container
189
+ ```
190
+
191
+ Registry behaviour is tested against `go-containerregistry`'s in-process
192
+ registry, so the suite doesn't touch the network or carry recorded fixtures.
193
+
194
+ ## License
195
+
196
+ MIT. See [LICENSE](LICENSE).
197
+
198
+ `src/docker_devtools/_find.py` adapts the binary-discovery search order from
199
+ [uv](https://github.com/astral-sh/uv), which is MIT OR Apache-2.0.
@@ -0,0 +1,166 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "strings"
9
+
10
+ "github.com/FlavioAmurrioCS/docker-devtools/dctx"
11
+ )
12
+
13
+ // ContextCmd groups the build-context commands. The listing itself is in the
14
+ // dctx package, whose output scripts/conformance.sh diffs against a real
15
+ // docker build for every fixture.
16
+ type ContextCmd struct {
17
+ Ls ContextLsCmd `cmd:"" help:"List the files Docker sends as the build context."`
18
+ Explain ContextExplainCmd `cmd:"" help:"Show which .dockerignore rule decided a path."`
19
+ }
20
+
21
+ type ContextLsCmd struct {
22
+ Path string `arg:"" optional:"" default:"." help:"Build context directory." type:"path"`
23
+
24
+ File string `short:"f" placeholder:"NAME" help:"Dockerfile name, which also selects <name>.dockerignore."`
25
+ Ignored bool `xor:"mode" help:"List the excluded files instead of the included ones."`
26
+ All bool `xor:"mode" help:"List every file, prefixed + for sent and - for excluded."`
27
+ Size bool `help:"Prefix each path with its size in bytes."`
28
+ Summary bool `help:"Print totals to stderr after the listing."`
29
+ JSON bool `name:"json" help:"Emit the full result as JSON."`
30
+ Zero bool `short:"0" help:"Separate paths with NUL, for xargs -0."`
31
+ }
32
+
33
+ func (c *ContextLsCmd) Run(st *Streams) error {
34
+ mode := dctx.ModeIncluded
35
+ switch {
36
+ case c.All:
37
+ mode = dctx.ModeAll
38
+ case c.Ignored:
39
+ mode = dctx.ModeIgnored
40
+ }
41
+
42
+ res, err := dctx.Walk(context.Background(), dctx.Options{
43
+ Context: c.Path,
44
+ Dockerfile: c.File,
45
+ Mode: mode,
46
+ })
47
+ if err != nil {
48
+ return err
49
+ }
50
+ for _, w := range res.Warnings {
51
+ fmt.Fprintln(st.Stderr, "warning:", w)
52
+ }
53
+ if c.JSON {
54
+ return writeJSON(st.Stdout, res)
55
+ }
56
+
57
+ sep := "\n"
58
+ if c.Zero {
59
+ sep = "\x00"
60
+ }
61
+ for _, e := range res.Entries {
62
+ var b strings.Builder
63
+ if c.All {
64
+ if e.Status == dctx.StatusIncluded {
65
+ b.WriteString("+ ")
66
+ } else {
67
+ b.WriteString("- ")
68
+ }
69
+ }
70
+ if c.Size {
71
+ fmt.Fprintf(&b, "%10d ", e.Size)
72
+ }
73
+ b.WriteString(e.Path)
74
+ b.WriteString(sep)
75
+ if _, err := stringWrite(st.Stdout, b.String()); err != nil {
76
+ return err
77
+ }
78
+ }
79
+ if c.Summary {
80
+ printSummary(st.Stderr, res)
81
+ }
82
+ return nil
83
+ }
84
+
85
+ type ContextExplainCmd struct {
86
+ Path string `arg:"" help:"Path to explain, relative to the context or absolute. It need not exist."`
87
+
88
+ Dir string `short:"C" default:"." placeholder:"DIR" help:"Build context directory." type:"path"`
89
+ File string `short:"f" placeholder:"NAME" help:"Dockerfile name, which also selects <name>.dockerignore."`
90
+ JSON bool `name:"json" help:"Emit the explanation as JSON."`
91
+ }
92
+
93
+ func (c *ContextExplainCmd) Run(st *Streams) error {
94
+ exp, err := dctx.Explain(dctx.Options{Context: c.Dir, Dockerfile: c.File}, c.Path)
95
+ if err != nil {
96
+ return err
97
+ }
98
+ if c.JSON {
99
+ return writeJSON(st.Stdout, exp)
100
+ }
101
+
102
+ fmt.Fprintf(st.Stdout, "%s: %s\n", exp.Path, exp.Status)
103
+ if !exp.Exists {
104
+ fmt.Fprintln(st.Stdout, " (path does not exist in the context)")
105
+ }
106
+ label := exp.Ignorefile
107
+ if label == "" {
108
+ label = "(no ignore file)"
109
+ }
110
+ if len(exp.Rules) == 0 {
111
+ fmt.Fprintf(st.Stdout, " no rule in %s matches; included by default\n", label)
112
+ return nil
113
+ }
114
+ for _, r := range exp.Rules {
115
+ effect := "ignored"
116
+ if r.Negated {
117
+ effect = "re-included"
118
+ }
119
+ line := fmt.Sprintf(" %s:%d", label, r.Line)
120
+ fmt.Fprintf(st.Stdout, "%-24s %-24s %s", line, r.Rule, effect)
121
+ if r.Decisive {
122
+ fmt.Fprint(st.Stdout, " (decisive)")
123
+ }
124
+ fmt.Fprintln(st.Stdout)
125
+ }
126
+ return nil
127
+ }
128
+
129
+ func printSummary(w io.Writer, res *dctx.Result) {
130
+ fmt.Fprintf(w, "included: %s, %s\n",
131
+ plural(res.Summary.Included.Files, "file"), humanBytes(res.Summary.Included.Bytes))
132
+ if res.Summary.Ignored != nil {
133
+ fmt.Fprintf(w, "ignored: %s, %s\n",
134
+ plural(res.Summary.Ignored.Files, "file"), humanBytes(res.Summary.Ignored.Bytes))
135
+ } else {
136
+ fmt.Fprintln(w, "ignored: not counted (ignored directories were skipped; use --all)")
137
+ }
138
+ }
139
+
140
+ func plural(n int64, noun string) string {
141
+ if n == 1 {
142
+ return fmt.Sprintf("%d %s", n, noun)
143
+ }
144
+ return fmt.Sprintf("%d %ss", n, noun)
145
+ }
146
+
147
+ func humanBytes(n int64) string {
148
+ const unit = 1024
149
+ if n < unit {
150
+ return fmt.Sprintf("%d B", n)
151
+ }
152
+ div, exp := int64(unit), 0
153
+ for v := n / unit; v >= unit; v /= unit {
154
+ div *= unit
155
+ exp++
156
+ }
157
+ return fmt.Sprintf("%.1f %ciB", float64(n)/float64(div), "KMGTPE"[exp])
158
+ }
159
+
160
+ func writeJSON(w io.Writer, v any) error {
161
+ enc := json.NewEncoder(w)
162
+ enc.SetIndent("", " ")
163
+ return enc.Encode(v)
164
+ }
165
+
166
+ func stringWrite(w io.Writer, s string) (int, error) { return io.WriteString(w, s) }