Skip to content

Container image retention #127

Container image retention

Container image retention #127

---
name: 'Container image retention'
on: # yamllint disable-line rule:truthy
push:
branches:
- 'main'
paths:
- '.github/workflows/container_image_retention.yml'
schedule:
- cron: '30 3 * * *'
workflow_dispatch: {}
permissions:
contents: 'read'
concurrency:
group: 'ci-${{ github.workflow }}-${{ github.ref }}'
cancel-in-progress: false
env:
# full image name including the complete path of the registry
IMAGE_NAME: 'ghcr.io/${{ github.repository_owner }}/ansible-role-file_deployment-jekyll'
# number of versions to keep
KEEP_VERSIONS: 3
# regular expression for tags to filter/ignore
IGNORE_TAGS: '^(latest|buildcache)$|^sha([[:digit:]]+?)?-'
# additional tags to resolve and keep the digests from, comma-seperated
ADDITIONAL_KEEP_TAGS: 'latest,buildcache'
jobs:
check-user-permissions:
runs-on: 'ubuntu-24.04'
permissions:
contents: 'read'
outputs:
require-result: '${{ steps.check-access.outputs.require-result }}'
steps:
- name: 'Harden Runner'
uses: 'step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7' # v2.10.1
with:
egress-policy: 'block'
allowed-endpoints: >
api.github.com:443
github.com:443
- name: 'Get User Permissions'
id: 'check-access'
uses: 'actions-cool/check-user-permission@956b2e73cdfe3bcb819bb7225e490cb3b18fd76e' # v2.2.1
with:
require: 'write'
username: '${{ github.triggering_actor }}'
env:
GITHUB_TOKEN: '${{ secrets.token }}'
- name: 'Check User Permission'
if: "steps.check-access.outputs.require-result == 'false'"
shell: 'bash'
run: |
# fail if:
# - a variable is unbound
# - any command fails
# - a command in a pipe fails
# - a command in a sub-shell fails
set -Eeuo pipefail
# enable debug if runner runs in debug
[[ "${{ runner.debug }}" -ne 1 ]] || {
echo "INFO: Enabling bash trace";
set -x;
};
echo "${{ github.triggering_actor }} does not have permissions on this repo."
echo "Current permission level is ${{ steps.check-access.outputs.user-permission }}"
echo "Job originally triggered by ${{ github.actor }}"
prepare-vars:
name: 'Prepare variables for building the container image'
if: "needs.check-user-permissions.outputs.require-result == 'true'"
needs: 'check-user-permissions'
runs-on: 'ubuntu-24.04'
container:
# yamllint disable-line rule:line-length
image: 'registry.access.redhat.com/ubi9/podman:9.4-14.1728871566@sha256:8b48dc911e206b26e79be0da8a393c245d5d2f1a67b50cc47cdbdcc8c690109d'
options: '--privileged'
outputs:
action-debug: '${{ steps.prepare-vars.outputs.action-debug }}'
short-image-name: '${{ steps.prepare-vars.outputs.short-image-name }}'
skip-digests: '${{ steps.prepare-vars.outputs.skip-digests }}'
skip-tags: '${{ steps.prepare-vars.outputs.skip-tags }}'
steps:
- name: 'Prepare variables'
id: 'prepare-vars'
shell: 'bash'
run: |
# fail if:
# - a variable is unbound
# - any command fails
# - a command in a pipe fails
# - a command in a sub-shell fails
set -Eeuo pipefail
echo "action-debug=container_retention_policy=info" >> "${GITHUB_OUTPUT}"
# enable debug if runner runs in debug
[[ "${{ runner.debug }}" -ne 1 ]] || {
echo "INFO: Enabling bash trace";
set -x;
echo "INFO: Defining 'action-debug' with: container_retention_policy=debug";
echo "action-debug=container_retention_policy=debug" >> "${GITHUB_OUTPUT}";
};
# install jq
dnf install -y jq
# podman arguments to search for tags
declare -ra podmanArguments=(
"search"
"--list-tags"
"--no-trunc"
"--limit"
"999999"
"--format"
"{{.Tag}}"
"${{ env.IMAGE_NAME }}"
)
# retrieve all tags for the image, sort it reverse and keep the last N versions
read -ra keepTags <<<"$(podman "${podmanArguments[@]}" | \
grep -vE "${{ env.IGNORE_TAGS }}" | \
sort -Vr | \
head -n "${{ env.KEEP_VERSIONS }}" | \
xargs \
)"
# add additional user tags to keep
IFS="," read -ra additionalKeepTags <<< "${{ env.ADDITIONAL_KEEP_TAGS }}"
declare -a allKeepTags=("${keepTags[@]}" "${additionalKeepTags[@]}")
# iterate over all tags and retrieve their digests
declare -a skipDigests=()
for keepTag in "${allKeepTags[@]}"; do
echo "INFO: Retrieving digests for tag ${keepTag}"
while read -r digest; do
echo "INFO: Found digest '${digest}'"
skipDigests+=("${digest}")
done < <(podman manifest inspect "${{ env.IMAGE_NAME }}:${keepTag}" | jq -r '.manifests[] | .digest')
# ^ extract the digests of a tag
done
# prepare tags to skip
declare -a skipTags=()
for tag in "${allKeepTags[@]}"; do
skipTags+=("!${tag}")
done
echo "INFO: Defining 'skip-tags' with value: ${skipTags[@]}"
echo "skip-tags=${skipTags[@]}" >> "${GITHUB_OUTPUT}"
# save the digests for a later step
echo "INFO: Defining 'skip-digests' with value: ${skipDigests[@]}"
echo "skip-digests=${skipDigests[@]}" >> "${GITHUB_OUTPUT}"
declare -r imageName="${{ env.IMAGE_NAME }}"
declare -r shortImageName="${imageName##*/}"
# save the image short name (without registry path)
echo "INFO: Defininig 'short-image-name' with value: ${shortImageName}"
echo "short-image-name=${shortImageName}" >> "${GITHUB_OUTPUT}"
container-image-retention:
name: 'Clean old container images'
if: "needs.check-user-permissions.outputs.require-result == 'true'"
needs:
- 'check-user-permissions'
- 'prepare-vars'
runs-on: 'ubuntu-24.04'
permissions:
packages: 'write'
steps:
- name: 'Harden Runner'
uses: 'step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7' # v2.10.1
with:
disable-sudo: true
egress-policy: 'block'
allowed-endpoints: >
api.github.com:443
github.com:443
- name: 'Delete container versions'
uses: 'snok/container-retention-policy@4f22ef80902ad409ed55a99dc5133cc1250a0d03' # v3.0.0
with:
account: 'user'
cut-off: '1m'
image-names: '${{ needs.prepare-vars.outputs.short-image-name }}'
image-tags: '${{ needs.prepare-vars.outputs.skip-tags }}'
keep-n-most-recent: '${{ env.KEEP_VERSIONS }}'
token: '${{ secrets.GITHUB_TOKEN }}'
tag-selection: 'both'
skip-shas: '${{ needs.prepare-vars.outputs.skip-digests }}'
timestamp-to-use: 'created_at'
dry-run: false
rust-log: '${{ needs.prepare-vars.outputs.action-debug }}'
...