From b6d34bcc5609acc65880a2c5fb54f7951df14c99 Mon Sep 17 00:00:00 2001 From: Nikita COEUR Date: Fri, 30 Jan 2026 15:59:12 +0100 Subject: [PATCH 1/2] feat(prune-pull-requests-images-tags): add preserve-tags-filter --- .../workflows/prune-pull-requests-images-tags.yml | 9 +++++++++ .../prune-pull-requests-image-tags/action.yml | 9 +++++++++ .../docker/prune-pull-requests-image-tags/index.js | 14 ++++++++++++++ 3 files changed, 32 insertions(+) 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..3607a1f1 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,6 +126,17 @@ async function getTagsToDeleteFromPackageVersion({ return []; } + // Filter out tags that should be preserved + if (preserveTagsFilter && preserveTagsFilter.length > 0) { + const preservedTags = tags.filter((tag) => tag.match(preserveTagsFilter)); + if (preservedTags.length > 0) { + core.debug( + `Preserving tags matching filter ${preserveTagsFilter}: ${preservedTags.join(", ")}`, + ); + } + return tags.filter((tag) => !tag.match(preserveTagsFilter)); + } + return tags; } From 7f86148550ebbe9c3508a2813f37a3ae23bc6c0b Mon Sep 17 00:00:00 2001 From: Nikita COEUR Date: Sat, 31 Jan 2026 10:55:51 +0100 Subject: [PATCH 2/2] refactor(prune-pull-requests-image-tags): add preserve filter validation and error handling Addresses PR review comments by adding robust regex validation and error handling: - Extract preserve filter logic into dedicated filterPreservedTags function - Validate preserve filter regex before use with try-catch - Use core.setFailed for invalid regex patterns - Handle regex matching errors gracefully with appropriate warnings - Improve code modularity and maintainability --- .../prune-pull-requests-image-tags/index.js | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/actions/docker/prune-pull-requests-image-tags/index.js b/actions/docker/prune-pull-requests-image-tags/index.js index 3607a1f1..3bde4940 100644 --- a/actions/docker/prune-pull-requests-image-tags/index.js +++ b/actions/docker/prune-pull-requests-image-tags/index.js @@ -126,18 +126,7 @@ async function getTagsToDeleteFromPackageVersion({ return []; } - // Filter out tags that should be preserved - if (preserveTagsFilter && preserveTagsFilter.length > 0) { - const preservedTags = tags.filter((tag) => tag.match(preserveTagsFilter)); - if (preservedTags.length > 0) { - core.debug( - `Preserving tags matching filter ${preserveTagsFilter}: ${preservedTags.join(", ")}`, - ); - } - return tags.filter((tag) => !tag.match(preserveTagsFilter)); - } - - return tags; + return filterPreservedTags({ tags, preserveTagsFilter, core }); } function getPullRequestRelatedTags({ tags, pullRequestTagFilter }) { @@ -158,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)) {