diff --git a/.github/workflows/prune-pull-requests-images-tags.yml b/.github/workflows/prune-pull-requests-images-tags.yml index 9a81e5ac..c5861dc5 100644 --- a/.github/workflows/prune-pull-requests-images-tags.yml +++ b/.github/workflows/prune-pull-requests-images-tags.yml @@ -37,6 +37,14 @@ on: # yamllint disable-line rule:truthy default: "^pr-([0-9]+)(?:-|$)" type: string required: false + preserve-tags-filter: + description: | + Optional regular expression to match tags that should be preserved (not deleted). + Tags matching this pattern will never be deleted, even if they are on a package version with PR tags. + Example: "^v.*" to preserve version tags like v1.0.0, v2.1.3, etc. + default: "" + type: string + required: false permissions: {} @@ -105,6 +113,7 @@ jobs: with: image: ${{ matrix.image }} pull-request-tag-filter: ${{ inputs.pull-request-tag-filter }} + preserve-tags-filter: ${{ inputs.preserve-tags-filter }} - uses: hoverkraft-tech/ci-github-common/actions/local-workflow-actions@b17226e57c8ef31f860719766656ebb6df017218 # 0.31.6 if: always() && steps.local-workflow-actions.outputs.repository diff --git a/actions/docker/prune-pull-requests-image-tags/action.yml b/actions/docker/prune-pull-requests-image-tags/action.yml index 23386d72..b02d7870 100644 --- a/actions/docker/prune-pull-requests-image-tags/action.yml +++ b/actions/docker/prune-pull-requests-image-tags/action.yml @@ -23,6 +23,13 @@ inputs: pull-request-tag-filter: description: "The regular expression to match pull request tags. Must have a capture group for the pull request number." default: "^pr-([0-9]+)(?:-|$)" + preserve-tags-filter: + description: | + Optional regular expression to match tags that should be preserved (not deleted). + Tags matching this pattern will never be deleted, even if they are on a package version with PR tags. + Example: "^v.*" to preserve version tags like v1.0.0, v2.1.3, etc. + required: false + default: "" github-token: description: | GitHub token with the folowing scopes: `pull-requests:read`, `packages:read` and `packages:delete`. @@ -73,6 +80,7 @@ runs: const imageName = `${{ steps.image-name.outputs.image-name }}`; const isOrganization = `${{ steps.is-organization-or-user.outputs.is-organization }}` === 'true'; + const preserveTagsFilter = `${{ inputs.preserve-tags-filter }}`; const script = require(`${{ github.action_path }}/index.js`) const tagsToDelete = await script({ @@ -81,6 +89,7 @@ runs: core, imageName, pullRequestTagFilter, + preserveTagsFilter, isOrganization, }); diff --git a/actions/docker/prune-pull-requests-image-tags/index.js b/actions/docker/prune-pull-requests-image-tags/index.js index d83f0626..3bde4940 100644 --- a/actions/docker/prune-pull-requests-image-tags/index.js +++ b/actions/docker/prune-pull-requests-image-tags/index.js @@ -4,6 +4,7 @@ module.exports = async ({ core, imageName, pullRequestTagFilter, + preserveTagsFilter, isOrganization, }) => { const repositoryOwner = `${context.repo.owner}`.toLowerCase(); @@ -33,6 +34,7 @@ module.exports = async ({ context, core, pullRequestTagFilter, + preserveTagsFilter, packageVersion, }), ), @@ -84,6 +86,7 @@ async function getTagsToDeleteFromPackageVersion({ context, core, pullRequestTagFilter, + preserveTagsFilter, packageVersion, }) { const tags = packageVersion.metadata.container.tags; @@ -123,7 +126,7 @@ async function getTagsToDeleteFromPackageVersion({ return []; } - return tags; + return filterPreservedTags({ tags, preserveTagsFilter, core }); } function getPullRequestRelatedTags({ tags, pullRequestTagFilter }) { @@ -144,6 +147,50 @@ function getPullRequestRelatedTags({ tags, pullRequestTagFilter }) { return [...new Set(pullRequestRelatedTags)]; } +function filterPreservedTags({ tags, preserveTagsFilter, core }) { + if (!preserveTagsFilter || preserveTagsFilter.length === 0) { + return tags; + } + + let preserveRegex; + try { + preserveRegex = new RegExp(preserveTagsFilter); + } catch (error) { + core.setFailed( + `Invalid preserve tag filter regex "${preserveTagsFilter}". Error: ${error.message}`, + ); + throw error; + } + + const preservedTags = tags.filter((tag) => { + try { + return preserveRegex.test(tag); + } catch (error) { + core.warning( + `Error while applying preserve tag filter regex "${preserveTagsFilter}" to tag "${tag}". Treating tag as not preserved. Error: ${error.message}`, + ); + return false; + } + }); + + if (preservedTags.length > 0) { + core.debug( + `Preserving tags matching filter ${preserveTagsFilter}: ${preservedTags.join(", ")}`, + ); + } + + return tags.filter((tag) => { + try { + return !preserveRegex.test(tag); + } catch (error) { + core.warning( + `Error while applying preserve tag filter regex "${preserveTagsFilter}" to tag "${tag}". Keeping tag for safety. Error: ${error.message}`, + ); + return true; + } + }); +} + const closedPullRequests = new Map(); async function isPullRequestClosed({ github, context, pullRequestNumber }) { if (!closedPullRequests.has(pullRequestNumber)) {