Container image retention #116
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
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 }}' | |
... |