Merge pull request '[bug-bkhzrg4dzz00] feat(publisher): allow retained image tags' (#1) from architect/bug-bkhzrg4dzz00/optional-pruning into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
name: pr
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
jobs:
|
||||||
|
contract:
|
||||||
|
runs-on: fritzlab
|
||||||
|
timeout-minutes: 5
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: validate composite contract without registry operations
|
||||||
|
run: |
|
||||||
|
python3 -m venv /tmp/image-push-tests
|
||||||
|
/tmp/image-push-tests/bin/pip install --quiet -r tests/requirements.txt
|
||||||
|
/tmp/image-push-tests/bin/python -m unittest discover -s tests
|
||||||
|
git diff --check
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
__pycache__/
|
||||||
@@ -31,6 +31,7 @@ local Docker daemon, image-push uploads it.
|
|||||||
| `org` | yes | — | Gitea org for package API (`fritzlab`, `dns`). |
|
| `org` | yes | — | Gitea org for package API (`fritzlab`, `dns`). |
|
||||||
| `name` | yes | — | Package name as registered in the registry. |
|
| `name` | yes | — | Package name as registered in the registry. |
|
||||||
| `latest` | no | `true` | Also push a `:latest` tag. |
|
| `latest` | no | `true` | Also push a `:latest` tag. |
|
||||||
|
| `prune` | no | `true` | Prune old numeric registry tags. Set `false` for retained or independently pinned artifacts. |
|
||||||
| `keep` | no | `3` | Numeric tags to retain. Older are deleted. |
|
| `keep` | no | `3` | Numeric tags to retain. Older are deleted. |
|
||||||
|
|
||||||
## Behavior
|
## Behavior
|
||||||
@@ -38,6 +39,16 @@ local Docker daemon, image-push uploads it.
|
|||||||
1. `docker login code.fritzlab.net` as `ci-bot`.
|
1. `docker login code.fritzlab.net` as `ci-bot`.
|
||||||
2. `docker push <image>:<tag>`.
|
2. `docker push <image>:<tag>`.
|
||||||
3. If `latest=true`, also `docker push <image>:latest`.
|
3. If `latest=true`, also `docker push <image>:latest`.
|
||||||
4. Prune: list numeric tags from Gitea package API, keep the newest `keep`,
|
4. If `prune=true`, list numeric tags from Gitea package API, keep the newest `keep`,
|
||||||
delete the rest. Failures here do not fail the workflow
|
delete the rest. Failures here do not fail the workflow
|
||||||
(`continue-on-error: true`).
|
(`continue-on-error: true`).
|
||||||
|
|
||||||
|
`prune` accepts only the strings `true` and `false`; invalid input fails before
|
||||||
|
registry login or push. With `false`, remote package listing and deletion are
|
||||||
|
skipped. Image upload and local Docker cleanup remain unchanged. The action does
|
||||||
|
not discover deployment pins; owners choosing pruning must account for that
|
||||||
|
retention contract.
|
||||||
|
|
||||||
|
Run `python3 -m unittest discover -s tests` with the pinned test dependencies in
|
||||||
|
`tests/requirements.txt`. Tests inspect the composite action and execute only
|
||||||
|
its credential-free validation step; no registry or package deletion runs.
|
||||||
|
|||||||
+15
@@ -21,6 +21,10 @@ inputs:
|
|||||||
description: Also push a :latest tag
|
description: Also push a :latest tag
|
||||||
required: false
|
required: false
|
||||||
default: 'true'
|
default: 'true'
|
||||||
|
prune:
|
||||||
|
description: Prune old numeric registry tags; false retains every remote tag
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
keep:
|
keep:
|
||||||
description: Numeric tags to retain after prune; older ones are deleted
|
description: Numeric tags to retain after prune; older ones are deleted
|
||||||
required: false
|
required: false
|
||||||
@@ -28,6 +32,16 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
|
- name: Validate pruning policy
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
PRUNE: ${{ inputs.prune }}
|
||||||
|
run: |
|
||||||
|
case "$PRUNE" in
|
||||||
|
true|false) ;;
|
||||||
|
*) echo "prune must be true or false" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
- name: Log in to registry
|
- name: Log in to registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -54,6 +68,7 @@ runs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Prune old tags
|
- name: Prune old tags
|
||||||
|
if: ${{ inputs.prune == 'true' }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
PyYAML==6.0.3
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class PruningContract(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.action = yaml.safe_load((Path(__file__).resolve().parents[1] / "action.yaml").read_text())
|
||||||
|
cls.steps = cls.action["runs"]["steps"]
|
||||||
|
|
||||||
|
def test_default_preserves_existing_callers_and_only_prune_is_conditional(self):
|
||||||
|
self.assertEqual(self.action["inputs"]["prune"]["default"], "true")
|
||||||
|
gated = [step for step in self.steps if "if" in step]
|
||||||
|
self.assertEqual(len(gated), 1)
|
||||||
|
self.assertEqual(gated[0]["name"], "Prune old tags")
|
||||||
|
self.assertEqual(gated[0]["if"], "${{ inputs.prune == 'true' }}")
|
||||||
|
remote = [step for step in self.steps if "/packages/" in step.get("run", "")]
|
||||||
|
self.assertEqual(remote, gated)
|
||||||
|
self.assertIn("docker push", next(step for step in self.steps if step["name"] == "Push")["run"])
|
||||||
|
self.assertNotIn("if", next(step for step in self.steps if step["name"] == "Untag local image"))
|
||||||
|
|
||||||
|
def test_invalid_policy_stops_before_credential_step(self):
|
||||||
|
validation = self.steps[0]
|
||||||
|
self.assertEqual(validation["name"], "Validate pruning policy")
|
||||||
|
self.assertEqual(validation["env"], {"PRUNE": "${{ inputs.prune }}"})
|
||||||
|
self.assertNotIn("uses", validation)
|
||||||
|
for value, success in (("true", True), ("false", True), ("False", False), ("", False), ("yes", False), ("false\ntrue", False)):
|
||||||
|
with self.subTest(value=value):
|
||||||
|
result = subprocess.run(["bash", "-c", validation["run"]], env={"PATH": os.defpath, "PRUNE": value}, capture_output=True)
|
||||||
|
self.assertEqual(result.returncode == 0, success)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user