releaseprobe 0.1.0__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.
- releaseprobe-0.1.0/.devcontainer/devcontainer-lock.json +9 -0
- releaseprobe-0.1.0/.devcontainer/devcontainer.json +24 -0
- releaseprobe-0.1.0/.github/dependabot.yml +10 -0
- releaseprobe-0.1.0/.github/workflows/release.yml +80 -0
- releaseprobe-0.1.0/.github/workflows/test.yml +39 -0
- releaseprobe-0.1.0/.gitignore +22 -0
- releaseprobe-0.1.0/LICENSE +21 -0
- releaseprobe-0.1.0/PKG-INFO +124 -0
- releaseprobe-0.1.0/README.md +93 -0
- releaseprobe-0.1.0/pyproject.toml +69 -0
- releaseprobe-0.1.0/src/releaseprobe/__init__.py +7 -0
- releaseprobe-0.1.0/src/releaseprobe/cli.py +77 -0
- releaseprobe-0.1.0/src/releaseprobe/labels.py +35 -0
- releaseprobe-0.1.0/src/releaseprobe/probe.py +54 -0
- releaseprobe-0.1.0/tests/__init__.py +0 -0
- releaseprobe-0.1.0/tests/data/homeassistant.json +26 -0
- releaseprobe-0.1.0/tests/test_cli.py +76 -0
- releaseprobe-0.1.0/tests/test_probe.py +86 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"features": {
|
|
3
|
+
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {
|
|
4
|
+
"version": "1.10.0",
|
|
5
|
+
"resolved": "ghcr.io/devcontainers/features/docker-outside-of-docker@sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e",
|
|
6
|
+
"integrity": "sha256:c2c2cf829505ead8e4892c88c31b6594ae94a2bbb209e16e1fac456c1a3a624e"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "releaseprobe",
|
|
3
|
+
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bookworm",
|
|
4
|
+
"postCreateCommand": "pip install -e '.[dev]'",
|
|
5
|
+
"customizations": {
|
|
6
|
+
"vscode": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"ms-python.python",
|
|
9
|
+
"ms-python.vscode-pylance",
|
|
10
|
+
"charliermarsh.ruff",
|
|
11
|
+
"ms-python.mypy-type-checker"
|
|
12
|
+
],
|
|
13
|
+
"settings": {
|
|
14
|
+
"python.testing.pytestEnabled": true,
|
|
15
|
+
"python.testing.pytestArgs": ["tests"],
|
|
16
|
+
"[python]": {
|
|
17
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
18
|
+
"editor.formatOnSave": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"remoteUser": "vscode"
|
|
24
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "src/releaseprobe/__init__.py"
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
release:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
contents: write
|
|
18
|
+
outputs:
|
|
19
|
+
released: ${{ steps.check.outputs.released }}
|
|
20
|
+
tag: ${{ steps.check.outputs.tag }}
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Determine whether the version was bumped
|
|
31
|
+
id: check
|
|
32
|
+
run: |
|
|
33
|
+
version=$(python -c "import re, pathlib; print(re.search(r'__version__\s*=\s*\"([^\"]+)\"', pathlib.Path('src/releaseprobe/__init__.py').read_text())[1])")
|
|
34
|
+
tag="v${version}"
|
|
35
|
+
echo "version=${version}" >> "$GITHUB_OUTPUT"
|
|
36
|
+
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
|
37
|
+
|
|
38
|
+
if git rev-parse "${tag}" >/dev/null 2>&1; then
|
|
39
|
+
echo "released=false" >> "$GITHUB_OUTPUT"
|
|
40
|
+
echo "Tag ${tag} already exists, skipping release."
|
|
41
|
+
else
|
|
42
|
+
echo "released=true" >> "$GITHUB_OUTPUT"
|
|
43
|
+
echo "Version bump detected, releasing ${tag}."
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
- name: Create tag and GitHub release
|
|
47
|
+
if: steps.check.outputs.released == 'true'
|
|
48
|
+
env:
|
|
49
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
50
|
+
TAG: ${{ steps.check.outputs.tag }}
|
|
51
|
+
run: |
|
|
52
|
+
git tag "${TAG}"
|
|
53
|
+
git push origin "${TAG}"
|
|
54
|
+
gh release create "${TAG}" \
|
|
55
|
+
--title "${TAG}" \
|
|
56
|
+
--generate-notes
|
|
57
|
+
|
|
58
|
+
publish:
|
|
59
|
+
needs: release
|
|
60
|
+
if: needs.release.outputs.released == 'true'
|
|
61
|
+
runs-on: ubuntu-latest
|
|
62
|
+
environment: pypi
|
|
63
|
+
permissions:
|
|
64
|
+
id-token: write
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
with:
|
|
68
|
+
ref: ${{ needs.release.outputs.tag }}
|
|
69
|
+
|
|
70
|
+
- uses: actions/setup-python@v5
|
|
71
|
+
with:
|
|
72
|
+
python-version: "3.12"
|
|
73
|
+
|
|
74
|
+
- name: Build distribution
|
|
75
|
+
run: |
|
|
76
|
+
pip install build
|
|
77
|
+
python -m build
|
|
78
|
+
|
|
79
|
+
- name: Publish to PyPI
|
|
80
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
cache: pip
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: pip install -e ".[dev]"
|
|
31
|
+
|
|
32
|
+
- name: Lint
|
|
33
|
+
run: ruff check .
|
|
34
|
+
|
|
35
|
+
- name: Type check
|
|
36
|
+
run: mypy
|
|
37
|
+
|
|
38
|
+
- name: Test
|
|
39
|
+
run: pytest
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
.env
|
|
10
|
+
|
|
11
|
+
# Testing / coverage
|
|
12
|
+
.coverage
|
|
13
|
+
.coverage.*
|
|
14
|
+
htmlcov/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
|
|
19
|
+
# Editors
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David
|
|
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,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: releaseprobe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find release notes, URLs and metadata from container labels and tags.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dkarv/releaseprobe
|
|
6
|
+
Project-URL: Repository, https://github.com/dkarv/releaseprobe
|
|
7
|
+
Project-URL: Issues, https://github.com/dkarv/releaseprobe/issues
|
|
8
|
+
Author-email: David Punzet <d.punzet@testbirds.de>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: container,docker,labels,oci,release-notes
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Topic :: Software Development :: Build Tools
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# releaseprobe
|
|
33
|
+
|
|
34
|
+
Find release notes, URLs and other release metadata for container images by
|
|
35
|
+
reading their labels. Inspired by [Renovate's docker datasource](https://docs.renovatebot.com/modules/datasource/docker/):
|
|
36
|
+
|
|
37
|
+
- [OCI image labels](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
38
|
+
(`org.opencontainers.image.*`), preferred.
|
|
39
|
+
- [label-schema.org](http://label-schema.org/rc1/) labels (`org.label-schema.*`),
|
|
40
|
+
as a fallback for older images.
|
|
41
|
+
|
|
42
|
+
releaseprobe does not talk to a registry or the Docker daemon itself - it
|
|
43
|
+
takes the labels you already have (from `docker inspect`, `skopeo inspect`,
|
|
44
|
+
`crane config`, the registry HTTP API, a Kubernetes pod spec, ...) as input
|
|
45
|
+
and tells you what release metadata they contain.
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install releaseprobe
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
Pass labels as a JSON object, e.g. straight from `docker inspect`:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
docker inspect --format '{{json .Config.Labels}}' ghcr.io/org/app:1.2.3 \
|
|
59
|
+
| releaseprobe
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
version: 1.2.3
|
|
64
|
+
source: https://github.com/org/app
|
|
65
|
+
url: -
|
|
66
|
+
documentation: -
|
|
67
|
+
revision: abc123
|
|
68
|
+
release notes: https://github.com/org/app
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A path to a JSON file works too (`releaseprobe labels.json`); omit the path or
|
|
72
|
+
pass `-` to read from stdin. Add `--json` for machine-readable output.
|
|
73
|
+
|
|
74
|
+
## Library usage
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from releaseprobe import probe_labels
|
|
78
|
+
|
|
79
|
+
info = probe_labels({"org.opencontainers.image.source": "https://github.com/org/app"})
|
|
80
|
+
info.release_notes_url() # "https://github.com/org/app"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -m venv .venv
|
|
87
|
+
source .venv/bin/activate
|
|
88
|
+
pip install -e ".[dev]"
|
|
89
|
+
|
|
90
|
+
pytest
|
|
91
|
+
ruff check .
|
|
92
|
+
mypy
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Dev container
|
|
96
|
+
|
|
97
|
+
A [dev container](.devcontainer/devcontainer.json) is included so you can try
|
|
98
|
+
releaseprobe in VS Code: open the repo and choose "Reopen in Container"
|
|
99
|
+
(needs the Dev Containers extension). It gives you a Python 3.12 environment
|
|
100
|
+
with the project installed in editable mode.
|
|
101
|
+
|
|
102
|
+
The container itself has no Docker CLI, so to try releaseprobe against a
|
|
103
|
+
real image, inspect it from your host and drop the labels into the workspace
|
|
104
|
+
(they land in the container too, since the workspace folder is mounted in):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# on your host
|
|
108
|
+
docker inspect --format '{{json .Config.Labels}}' ghcr.io/grafana/grafana:11.2.0 > labels.json
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# inside the dev container
|
|
113
|
+
releaseprobe labels.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Releasing
|
|
117
|
+
|
|
118
|
+
Bump the `__version__` string in `src/releaseprobe/__init__.py` and merge to
|
|
119
|
+
`main`. The CI pipeline detects the version bump, tags the commit, creates a
|
|
120
|
+
GitHub release with auto-generated notes, and publishes the package to PyPI.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# releaseprobe
|
|
2
|
+
|
|
3
|
+
Find release notes, URLs and other release metadata for container images by
|
|
4
|
+
reading their labels. Inspired by [Renovate's docker datasource](https://docs.renovatebot.com/modules/datasource/docker/):
|
|
5
|
+
|
|
6
|
+
- [OCI image labels](https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
7
|
+
(`org.opencontainers.image.*`), preferred.
|
|
8
|
+
- [label-schema.org](http://label-schema.org/rc1/) labels (`org.label-schema.*`),
|
|
9
|
+
as a fallback for older images.
|
|
10
|
+
|
|
11
|
+
releaseprobe does not talk to a registry or the Docker daemon itself - it
|
|
12
|
+
takes the labels you already have (from `docker inspect`, `skopeo inspect`,
|
|
13
|
+
`crane config`, the registry HTTP API, a Kubernetes pod spec, ...) as input
|
|
14
|
+
and tells you what release metadata they contain.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install releaseprobe
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Pass labels as a JSON object, e.g. straight from `docker inspect`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
docker inspect --format '{{json .Config.Labels}}' ghcr.io/org/app:1.2.3 \
|
|
28
|
+
| releaseprobe
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
version: 1.2.3
|
|
33
|
+
source: https://github.com/org/app
|
|
34
|
+
url: -
|
|
35
|
+
documentation: -
|
|
36
|
+
revision: abc123
|
|
37
|
+
release notes: https://github.com/org/app
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A path to a JSON file works too (`releaseprobe labels.json`); omit the path or
|
|
41
|
+
pass `-` to read from stdin. Add `--json` for machine-readable output.
|
|
42
|
+
|
|
43
|
+
## Library usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from releaseprobe import probe_labels
|
|
47
|
+
|
|
48
|
+
info = probe_labels({"org.opencontainers.image.source": "https://github.com/org/app"})
|
|
49
|
+
info.release_notes_url() # "https://github.com/org/app"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m venv .venv
|
|
56
|
+
source .venv/bin/activate
|
|
57
|
+
pip install -e ".[dev]"
|
|
58
|
+
|
|
59
|
+
pytest
|
|
60
|
+
ruff check .
|
|
61
|
+
mypy
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Dev container
|
|
65
|
+
|
|
66
|
+
A [dev container](.devcontainer/devcontainer.json) is included so you can try
|
|
67
|
+
releaseprobe in VS Code: open the repo and choose "Reopen in Container"
|
|
68
|
+
(needs the Dev Containers extension). It gives you a Python 3.12 environment
|
|
69
|
+
with the project installed in editable mode.
|
|
70
|
+
|
|
71
|
+
The container itself has no Docker CLI, so to try releaseprobe against a
|
|
72
|
+
real image, inspect it from your host and drop the labels into the workspace
|
|
73
|
+
(they land in the container too, since the workspace folder is mounted in):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# on your host
|
|
77
|
+
docker inspect --format '{{json .Config.Labels}}' ghcr.io/grafana/grafana:11.2.0 > labels.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# inside the dev container
|
|
82
|
+
releaseprobe labels.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Releasing
|
|
86
|
+
|
|
87
|
+
Bump the `__version__` string in `src/releaseprobe/__init__.py` and merge to
|
|
88
|
+
`main`. The CI pipeline detects the version bump, tags the commit, creates a
|
|
89
|
+
GitHub release with auto-generated notes, and publishes the package to PyPI.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "releaseprobe"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Find release notes, URLs and metadata from container labels and tags."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "David Punzet", email = "d.punzet@testbirds.de" }]
|
|
13
|
+
keywords = ["docker", "oci", "container", "release-notes", "labels"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Build Tools",
|
|
25
|
+
"Topic :: Utilities",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"pytest-cov>=5.0",
|
|
33
|
+
"ruff>=0.6",
|
|
34
|
+
"mypy>=1.11",
|
|
35
|
+
"build>=1.2",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/dkarv/releaseprobe"
|
|
40
|
+
Repository = "https://github.com/dkarv/releaseprobe"
|
|
41
|
+
Issues = "https://github.com/dkarv/releaseprobe/issues"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
releaseprobe = "releaseprobe.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
path = "src/releaseprobe/__init__.py"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/releaseprobe"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
addopts = "--cov=releaseprobe --cov-report=term-missing"
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
|
|
56
|
+
[tool.coverage.run]
|
|
57
|
+
source = ["releaseprobe"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
target-version = "py310"
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.10"
|
|
68
|
+
strict = true
|
|
69
|
+
files = ["src"]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Command line interface for releaseprobe."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import dataclasses
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from typing import TextIO
|
|
10
|
+
|
|
11
|
+
from releaseprobe import __version__
|
|
12
|
+
from releaseprobe.probe import probe_labels
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="releaseprobe",
|
|
18
|
+
description=(
|
|
19
|
+
"Find release notes, URLs and metadata from container image labels. "
|
|
20
|
+
"Labels are read as a JSON object, e.g. the output of "
|
|
21
|
+
"`docker inspect --format '{{json .Config.Labels}}' <image>`."
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"labels_file",
|
|
27
|
+
nargs="?",
|
|
28
|
+
default="-",
|
|
29
|
+
help="Path to a JSON file with the image labels, or '-' to read from stdin (default).",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--json",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Print the full result as JSON instead of a human-readable summary.",
|
|
35
|
+
)
|
|
36
|
+
return parser
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_labels(path: str, stdin: TextIO) -> dict[str, str]:
|
|
40
|
+
if path == "-":
|
|
41
|
+
raw = stdin.read()
|
|
42
|
+
else:
|
|
43
|
+
with open(path, encoding="utf-8") as f:
|
|
44
|
+
raw = f.read()
|
|
45
|
+
data = json.loads(raw) if raw.strip() else {}
|
|
46
|
+
if not isinstance(data, dict):
|
|
47
|
+
raise ValueError("labels JSON must be an object of label key/value pairs")
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv: list[str] | None = None) -> int:
|
|
52
|
+
parser = build_parser()
|
|
53
|
+
args = parser.parse_args(argv)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
labels = _read_labels(args.labels_file, sys.stdin)
|
|
57
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
58
|
+
print(f"error: could not read labels: {exc}", file=sys.stderr)
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
info = probe_labels(labels)
|
|
62
|
+
|
|
63
|
+
if args.json:
|
|
64
|
+
print(json.dumps(dataclasses.asdict(info), indent=2))
|
|
65
|
+
return 0
|
|
66
|
+
|
|
67
|
+
print(f"version: {info.version or '-'}")
|
|
68
|
+
print(f"source: {info.source or '-'}")
|
|
69
|
+
print(f"url: {info.url or '-'}")
|
|
70
|
+
print(f"documentation: {info.documentation or '-'}")
|
|
71
|
+
print(f"revision: {info.revision or '-'}")
|
|
72
|
+
print(f"release notes: {info.release_notes_url() or '-'}")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Well-known container image label keys relevant to release metadata.
|
|
2
|
+
|
|
3
|
+
Two competing conventions are in use in the wild, mirroring how Renovate's
|
|
4
|
+
docker datasource reads them (see
|
|
5
|
+
https://docs.renovatebot.com/modules/datasource/docker/):
|
|
6
|
+
|
|
7
|
+
- OCI Image Format Specification (current):
|
|
8
|
+
https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
|
9
|
+
- label-schema.org (deprecated, still common on older images):
|
|
10
|
+
http://label-schema.org/rc1/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# OCI Image Format Specification annotations.
|
|
16
|
+
OCI_SOURCE = "org.opencontainers.image.source"
|
|
17
|
+
OCI_URL = "org.opencontainers.image.url"
|
|
18
|
+
OCI_DOCUMENTATION = "org.opencontainers.image.documentation"
|
|
19
|
+
OCI_VERSION = "org.opencontainers.image.version"
|
|
20
|
+
OCI_REVISION = "org.opencontainers.image.revision"
|
|
21
|
+
OCI_VENDOR = "org.opencontainers.image.vendor"
|
|
22
|
+
OCI_TITLE = "org.opencontainers.image.title"
|
|
23
|
+
OCI_DESCRIPTION = "org.opencontainers.image.description"
|
|
24
|
+
OCI_CREATED = "org.opencontainers.image.created"
|
|
25
|
+
|
|
26
|
+
# label-schema.org annotations (deprecated), used as a fallback when the OCI
|
|
27
|
+
# label is absent.
|
|
28
|
+
LABEL_SCHEMA_VCS_URL = "org.label-schema.vcs-url"
|
|
29
|
+
LABEL_SCHEMA_VCS_REF = "org.label-schema.vcs-ref"
|
|
30
|
+
LABEL_SCHEMA_VERSION = "org.label-schema.version"
|
|
31
|
+
|
|
32
|
+
# Field -> ordered list of label keys to check, OCI first, label-schema as fallback.
|
|
33
|
+
SOURCE_KEYS = (OCI_SOURCE, LABEL_SCHEMA_VCS_URL)
|
|
34
|
+
REVISION_KEYS = (OCI_REVISION, LABEL_SCHEMA_VCS_REF)
|
|
35
|
+
VERSION_KEYS = (OCI_VERSION, LABEL_SCHEMA_VERSION)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Derive release metadata from a container image's labels.
|
|
2
|
+
|
|
3
|
+
The labels themselves are expected to already have been collected by the
|
|
4
|
+
caller (e.g. via `docker inspect`, `skopeo inspect`, `crane config`, the
|
|
5
|
+
registry HTTP API, or a Kubernetes pod spec) - this module only interprets
|
|
6
|
+
them, following the same label conventions as Renovate's docker datasource:
|
|
7
|
+
https://docs.renovatebot.com/modules/datasource/docker/
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
from releaseprobe import labels as label_keys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _first(labels: Mapping[str, str], keys: tuple[str, ...]) -> str | None:
|
|
19
|
+
for key in keys:
|
|
20
|
+
value = labels.get(key)
|
|
21
|
+
if value:
|
|
22
|
+
return value
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ReleaseInfo:
|
|
28
|
+
version: str | None = None
|
|
29
|
+
source: str | None = None
|
|
30
|
+
url: str | None = None
|
|
31
|
+
documentation: str | None = None
|
|
32
|
+
revision: str | None = None
|
|
33
|
+
raw_labels: dict[str, str] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
def release_notes_url(self) -> str | None:
|
|
36
|
+
"""Best-effort guess at a release notes / changelog URL.
|
|
37
|
+
|
|
38
|
+
Prefers the VCS source repository (as Renovate does when deriving a
|
|
39
|
+
changelog), then falls back to the generic project URL and finally
|
|
40
|
+
the documentation URL.
|
|
41
|
+
"""
|
|
42
|
+
return self.source or self.url or self.documentation
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def probe_labels(labels: Mapping[str, str]) -> ReleaseInfo:
|
|
46
|
+
"""Extract release metadata from a mapping of container image labels."""
|
|
47
|
+
return ReleaseInfo(
|
|
48
|
+
version=_first(labels, label_keys.VERSION_KEYS),
|
|
49
|
+
source=_first(labels, label_keys.SOURCE_KEYS),
|
|
50
|
+
url=labels.get(label_keys.OCI_URL),
|
|
51
|
+
documentation=labels.get(label_keys.OCI_DOCUMENTATION),
|
|
52
|
+
revision=_first(labels, label_keys.REVISION_KEYS),
|
|
53
|
+
raw_labels=dict(labels),
|
|
54
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"io.hass.base.image": "ghcr.io/home-assistant/base:3.24",
|
|
3
|
+
"io.hass.arch": "amd64",
|
|
4
|
+
"org.opencontainers.image.url": "https://www.home-assistant.io/",
|
|
5
|
+
"io.hass.type": "core",
|
|
6
|
+
"io.hass.base.name": "python",
|
|
7
|
+
"io.hass.version": "2026.9.2",
|
|
8
|
+
"org.opencontainers.image.version": "2026.9.2",
|
|
9
|
+
"org.opencontainers.image.authors": "The Home Assistant Authors",
|
|
10
|
+
"org.opencontainers.image.source": "https://github.com/home-assistant/core",
|
|
11
|
+
"com.docker.compose.container-number": "1",
|
|
12
|
+
"com.docker.compose.replace": "homeassistant",
|
|
13
|
+
"com.docker.compose.service": "homeassistant",
|
|
14
|
+
"org.opencontainers.image.documentation": "https://www.home-assistant.io/docs/",
|
|
15
|
+
"org.opencontainers.image.licenses": "Apache-2.0",
|
|
16
|
+
"com.docker.compose.project.working_dir": "/var/misc/temp/periphery/repos/komodo/homeassistant",
|
|
17
|
+
"org.opencontainers.image.description": "Open-source home automation platform running on Python 3",
|
|
18
|
+
"com.docker.compose.version": "5.3.1",
|
|
19
|
+
"com.docker.compose.oneoff": "False",
|
|
20
|
+
"com.docker.compose.depends_on": "",
|
|
21
|
+
"io.hass.base.version": "2026.06.1",
|
|
22
|
+
"org.opencontainers.image.created": "2026-09-11 19:45:15+00:00",
|
|
23
|
+
"org.opencontainers.image.title": "Home Assistant",
|
|
24
|
+
"com.docker.compose.project": "homeassistant",
|
|
25
|
+
"io.hass.base.arch": "amd64"
|
|
26
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from releaseprobe import __version__
|
|
6
|
+
from releaseprobe.cli import main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_version_flag(capsys) -> None:
|
|
10
|
+
exit_code = None
|
|
11
|
+
try:
|
|
12
|
+
main(["--version"])
|
|
13
|
+
except SystemExit as exc:
|
|
14
|
+
exit_code = exc.code
|
|
15
|
+
|
|
16
|
+
assert exit_code == 0
|
|
17
|
+
assert __version__ in capsys.readouterr().out
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_main_prints_human_summary(tmp_path, capsys) -> None:
|
|
21
|
+
labels_file = tmp_path / "labels.json"
|
|
22
|
+
labels_file.write_text(
|
|
23
|
+
json.dumps(
|
|
24
|
+
{
|
|
25
|
+
"org.opencontainers.image.version": "1.2.3",
|
|
26
|
+
"org.opencontainers.image.source": "https://github.com/org/app",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
exit_code = main([str(labels_file)])
|
|
32
|
+
|
|
33
|
+
out = capsys.readouterr().out
|
|
34
|
+
assert exit_code == 0
|
|
35
|
+
assert "version: 1.2.3" in out
|
|
36
|
+
assert "release notes: https://github.com/org/app" in out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_main_prints_json(tmp_path, capsys) -> None:
|
|
40
|
+
labels_file = tmp_path / "labels.json"
|
|
41
|
+
labels_file.write_text(json.dumps({"org.opencontainers.image.version": "1.2.3"}))
|
|
42
|
+
|
|
43
|
+
exit_code = main([str(labels_file), "--json"])
|
|
44
|
+
|
|
45
|
+
out = capsys.readouterr().out
|
|
46
|
+
assert exit_code == 0
|
|
47
|
+
assert json.loads(out)["version"] == "1.2.3"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_main_reads_labels_from_stdin(monkeypatch, capsys) -> None:
|
|
51
|
+
import io
|
|
52
|
+
|
|
53
|
+
monkeypatch.setattr("sys.stdin", io.StringIO('{"org.opencontainers.image.version": "9.9.9"}'))
|
|
54
|
+
|
|
55
|
+
exit_code = main(["-"])
|
|
56
|
+
|
|
57
|
+
out = capsys.readouterr().out
|
|
58
|
+
assert exit_code == 0
|
|
59
|
+
assert "version: 9.9.9" in out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_main_reports_invalid_json(tmp_path, capsys) -> None:
|
|
63
|
+
labels_file = tmp_path / "labels.json"
|
|
64
|
+
labels_file.write_text("not json")
|
|
65
|
+
|
|
66
|
+
exit_code = main([str(labels_file)])
|
|
67
|
+
|
|
68
|
+
assert exit_code == 1
|
|
69
|
+
assert "error" in capsys.readouterr().err
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_main_reports_missing_file(capsys) -> None:
|
|
73
|
+
exit_code = main(["/nonexistent/labels.json"])
|
|
74
|
+
|
|
75
|
+
assert exit_code == 1
|
|
76
|
+
assert "error" in capsys.readouterr().err
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from releaseprobe import labels
|
|
4
|
+
from releaseprobe.probe import probe_labels
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_probe_labels_reads_oci_annotations() -> None:
|
|
8
|
+
info = probe_labels(
|
|
9
|
+
{
|
|
10
|
+
labels.OCI_VERSION: "1.2.3",
|
|
11
|
+
labels.OCI_SOURCE: "https://github.com/org/app",
|
|
12
|
+
labels.OCI_URL: "https://app.example.com",
|
|
13
|
+
labels.OCI_DOCUMENTATION: "https://docs.example.com",
|
|
14
|
+
labels.OCI_REVISION: "abc123",
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assert info.version == "1.2.3"
|
|
19
|
+
assert info.source == "https://github.com/org/app"
|
|
20
|
+
assert info.url == "https://app.example.com"
|
|
21
|
+
assert info.documentation == "https://docs.example.com"
|
|
22
|
+
assert info.revision == "abc123"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_probe_labels_falls_back_to_label_schema() -> None:
|
|
26
|
+
info = probe_labels(
|
|
27
|
+
{
|
|
28
|
+
labels.LABEL_SCHEMA_VERSION: "1.0.0",
|
|
29
|
+
labels.LABEL_SCHEMA_VCS_URL: "https://github.com/org/legacy",
|
|
30
|
+
labels.LABEL_SCHEMA_VCS_REF: "def456",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
assert info.version == "1.0.0"
|
|
35
|
+
assert info.source == "https://github.com/org/legacy"
|
|
36
|
+
assert info.revision == "def456"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_probe_labels_prefers_oci_over_label_schema() -> None:
|
|
40
|
+
info = probe_labels(
|
|
41
|
+
{
|
|
42
|
+
labels.OCI_SOURCE: "https://github.com/org/app",
|
|
43
|
+
labels.LABEL_SCHEMA_VCS_URL: "https://github.com/org/legacy",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert info.source == "https://github.com/org/app"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_probe_labels_keeps_raw_labels() -> None:
|
|
51
|
+
raw = {labels.OCI_VERSION: "1.2.3"}
|
|
52
|
+
info = probe_labels(raw)
|
|
53
|
+
|
|
54
|
+
assert info.raw_labels == raw
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_release_notes_url_prefers_source_then_url_then_documentation() -> None:
|
|
58
|
+
assert (
|
|
59
|
+
probe_labels(
|
|
60
|
+
{
|
|
61
|
+
labels.OCI_SOURCE: "https://github.com/org/app",
|
|
62
|
+
labels.OCI_URL: "https://app.example.com",
|
|
63
|
+
labels.OCI_DOCUMENTATION: "https://docs.example.com",
|
|
64
|
+
}
|
|
65
|
+
).release_notes_url()
|
|
66
|
+
== "https://github.com/org/app"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assert (
|
|
70
|
+
probe_labels(
|
|
71
|
+
{
|
|
72
|
+
labels.OCI_URL: "https://app.example.com",
|
|
73
|
+
labels.OCI_DOCUMENTATION: "https://docs.example.com",
|
|
74
|
+
}
|
|
75
|
+
).release_notes_url()
|
|
76
|
+
== "https://app.example.com"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
assert (
|
|
80
|
+
probe_labels({labels.OCI_DOCUMENTATION: "https://docs.example.com"}).release_notes_url()
|
|
81
|
+
== "https://docs.example.com"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_release_notes_url_returns_none_when_absent() -> None:
|
|
86
|
+
assert probe_labels({}).release_notes_url() is None
|