ci: normalise branch names into container tags (#13249)

We are moving branches in this repository to the `<type>/<summary>`
convention, which the container registries cannot take verbatim: a tag
admits only `[A-Za-z0-9_.-]`, so pushing
`authelia/authelia:fix/oidc-claims` fails with `invalid reference
format` on both DockerHub and GHCR. The same branch name reaches
`authelia/integration-duo` and friends through `resolve_tag_suffix`, and
the `sed` in the `INTEGRATION` hook additionally breaks on the `/` since
it is that expression's own delimiter.

Branch names are now folded into a tag by replacing anything outside the
permitted set with a `-`, so `fix/oidc-claims` publishes as
`fix-oidc-claims`. The mapping is implemented once per language that
needs it and each copy points at the other: `sanitize_tag` in
`.buildkite/libs/common.sh` covers `integration.sh`, `grypescans.sh` and
the `INTEGRATION` hook, and `sanitizeTag` in
`cmd/authelia-scripts/cmd/docker.go` covers the manifests we deploy. The
announcement in `.github/probot.js` keeps naming the tag it always did,
with a filter folding `head.ref` before the template renders it, since
the body is a mustache template and mustache cannot transform a value
itself.

The tag reaper needs the mapping too, and silently. It deletes every
published tag with no branch of the same name, so once the tags are
folded but the branch names it compares them against are not, every
branch carrying a `/` looks abandoned and loses its images on the next
`master` build. The branch list is now folded the same way before the
comparison, and sorted afterwards since folding changes the collation
order `comm` depends on.

While in that function, the GHCR lookup is matched with `index` rather
than `contains`. `contains` is a substring test, so reaping
`fix-oidc-claims` also deleted `fix-oidc-claims-policy`; prefixed branch
names make shared prefixes considerably more likely. It also emits a
version once per tag that matched, where `index` emits it once.

Renovate no longer needs `branchPrefix`, which only existed because
`renovate/` could not be used as a tag, so it returns to its default and
`branchPrefixOld` retires the branches left on the old prefix. The
collapse guard in `resolve_tag_suffix` moves with it.

Signed-off-by: Amir Zarrinkafsh <3339418+nightah@users.noreply.github.com>
This commit is contained in:
Amir Zarrinkafsh authored and GitHub committed 2026-09-23 22:09:05 +00:00
1 parent 9e0d77d0a7
commit c11a808fc4
6 files changed
+65 -7

No files matched your search

+6 -2
View File
@@ -6,6 +6,9 @@
set +eu
# shellcheck source=/dev/null
source "$(dirname "${BASH_SOURCE[0]}")/../libs/common.sh"
cleanup_tags() {
local docker_list="${1}" github_list="${2}"
local tag i ghcr_version
@@ -14,7 +17,7 @@ cleanup_tags() {
echo "Removing tag ${tag} from docker.io"
curl -fsL --retry 3 -o /dev/null -X DELETE -H "Authorization: JWT ${authtoken}" "https://hub.docker.com/v2/repositories/authelia/authelia/tags/${tag}/"
for i in {1..5}; do
for ghcr_version in $(curl -fsL --retry 3 -H "Authorization: Bearer ${GHCR_PASSWORD}" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/authelia/packages/container/authelia/versions?page=${i}&per_page=100" | jq -j --arg tag "${tag}" '.[] | select(.metadata.container.tags[] | contains($tag)) | .metadata.container.tags[], ",", .id, "\n"'); do
for ghcr_version in $(curl -fsL --retry 3 -H "Authorization: Bearer ${GHCR_PASSWORD}" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/authelia/packages/container/authelia/versions?page=${i}&per_page=100" | jq -j --arg tag "${tag}" '.[] | select(.metadata.container.tags | index($tag)) | .metadata.container.tags[], ",", .id, "\n"'); do
IFS=',' read -ra tagid <<< "${ghcr_version}"
echo "Removing tag ${tagid[0]} with id ${tagid[1]} from ghcr.io"
curl -fsL --retry 3 -o /dev/null -X DELETE -H "Authorization: Bearer ${GHCR_PASSWORD}" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/authelia/packages/container/authelia/versions/${tagid[1]}"
@@ -123,7 +126,8 @@ if [[ ${BUILDKITE_LABEL} == ":docker: Deploy Manifest" ]] && [[ ${BUILDKITE_BRAN
echo "--- :docker: Removing tags for deleted branches"
dockerbranchtags=$(curl -fsL --retry 3 -H "Authorization: Bearer ${anontoken}" https://registry-1.docker.io/v2/authelia/authelia/tags/list | jq -r '.tags[] | select(startswith("PR") | not)' | sed -r '/^(latest|master|develop|v.*|([[:digit:]]+)\.?([[:digit:]]+)?\.?([[:digit:]]+)?)$/d' | sort)
githubbranches=$(curl -fs --retry 3 https://api.github.com/repos/authelia/authelia/branches?per_page=100 | jq -r '.[].name' | sort)
githubbranches=$(curl -fs --retry 3 https://api.github.com/repos/authelia/authelia/branches?per_page=100 | jq -r '.[].name')
githubbranches=$(sanitize_tag "${githubbranches}" | sort)
cleanup_tags "${dockerbranchtags}" "${githubbranches}"
echo "--- :docker: Removing tags for merged or closed pull requests"
+14 -3
View File
@@ -19,6 +19,17 @@
# each of them; disabled file-wide since it's a false positive here.
# shellcheck disable=SC2034
# Makes a branch name safe to use in a Docker tag. A Docker tag admits only [A-Za-z0-9_.-], so the "/" in the
# "<type>/<summary>" branch convention (and anything else out of that set) is folded down to "-": fix/oidc-claims
# becomes fix-oidc-claims. Applied line by line, so a list of branch names can be passed in one call.
#
# Every place that turns a branch name into a tag must agree on this mapping, including the ones outside this file:
# the tag reaper in hooks/post-command, which compares the published tags against the live branch names, and
# sanitizeTag() in cmd/authelia-scripts/cmd/docker.go, which tags the manifests this pushes.
sanitize_tag() {
echo "${1}" | sed -E 's/[^A-Za-z0-9_.-]+/-/g'
}
changed() {
local base_ref="${1}"
local path_pattern="${2}"
@@ -64,7 +75,7 @@ resolve_base_ref() {
# ":latest" tag resolve to the same image callers only need to
# override this when they explicitly want the literal "master"
# tag instead (grypescans.sh does).
# $2 - "true" (default) to collapse renovate-* branches to the literal
# $2 - "true" (default) to collapse renovate/* branches to the literal
# suffix "renovate", matching the image tag integration.sh actually
# builds under. Pass "false" to fall through to the literal branch
# name instead; grypescans.sh scans the main "authelia/authelia"
@@ -79,10 +90,10 @@ resolve_tag_suffix() {
local collapse_renovate="${2:-true}"
TAG_SUFFIX=""
if [[ "${collapse_renovate}" == "true" ]] && [[ "${BUILDKITE_BRANCH}" =~ ^renovate- ]]; then
if [[ "${collapse_renovate}" == "true" ]] && [[ "${BUILDKITE_BRANCH}" =~ ^renovate/ ]]; then
TAG_SUFFIX="renovate"
elif [[ "${BUILDKITE_BRANCH}" != "master" ]] && [[ ! "${BUILDKITE_BRANCH}" =~ .*:.* ]]; then
TAG_SUFFIX="${BUILDKITE_BRANCH}"
TAG_SUFFIX=$(sanitize_tag "${BUILDKITE_BRANCH}")
elif [[ "${BUILDKITE_BRANCH}" != "master" ]] && [[ "${BUILDKITE_BRANCH}" =~ .*:.* ]]; then
TAG_SUFFIX="PR${BUILDKITE_PULL_REQUEST}"
elif [[ "${BUILDKITE_BRANCH}" == "master" ]] && [[ "${BUILDKITE_PULL_REQUEST}" == "false" ]]; then
+7
View File
@@ -14,6 +14,13 @@ on("pull_request.opened")
return !excludedBranchPrefixes.test(context.payload.pull_request.head.ref);
})
.filter((context) => !context.payload.pull_request.title.startsWith("docs"))
// A container tag admits only [A-Za-z0-9_.-], so the ref is folded to the tag the image is published under.
.filter((context) => {
context.payload.pull_request.head.ref =
context.payload.pull_request.head.ref.replace(/[^A-Za-z0-9_.-]+/g, "-");
return true;
})
.comment(`## Artifacts
These changes are published for testing on Buildkite, DockerHub and GitHub Container Registry.
+1 -1
View File
@@ -6,7 +6,7 @@
":separatePatchReleases",
"workarounds:doNotUpgradeFromAlpineStableToEdge"
],
"branchPrefix": "renovate-",
"branchPrefixOld": "renovate-",
"rebaseWhen": "conflicted",
"ignorePresets": [
":prHourlyLimit2",
+6 -1
View File
@@ -27,6 +27,7 @@ var (
dockerTags = regexp.MustCompile(`v(?P<Patch>(?P<Minor>(?P<Major>\d+)\.\d+)\.\d+.*)`)
ignoredSuffixes = regexp.MustCompile("alpha|beta")
publicRepo = regexp.MustCompile(`.*:.*`)
tagUnsafe = regexp.MustCompile(`[^A-Za-z0-9_.-]+`)
tags = dockerTags.FindStringSubmatch(ciTag)
)
@@ -122,7 +123,7 @@ func cmdDockerPushManifestRun(_ *cobra.Command, _ []string) {
case ciBranch != masterTag && !publicRepo.MatchString(ciBranch):
login(docker, dockerhub)
login(docker, ghcr)
deployManifest(docker, cve, ciBranch)
deployManifest(docker, cve, sanitizeTag(ciBranch))
case ciBranch != masterTag && publicRepo.MatchString(ciBranch):
login(docker, dockerhub)
login(docker, ghcr)
@@ -138,6 +139,10 @@ func cmdDockerPushManifestRun(_ *cobra.Command, _ []string) {
}
}
func sanitizeTag(tag string) string {
return tagUnsafe.ReplaceAllString(tag, "-")
}
func checkContainerIsSupported(container string) {
for _, v := range containers {
if container == v {
+31
View File
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2026 Authelia
//
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSanitizeTagShouldMakeBranchNamesSafeForDockerTags(t *testing.T) {
testCases := []struct {
name string
have string
expected string
}{
{"ShouldFoldTheTypeSeparator", "fix/oidc-claims", "fix-oidc-claims"},
{"ShouldFoldEveryRunToOneSeparator", "renovate/docker-haproxy/3.x", "renovate-docker-haproxy-3.x"},
{"ShouldPreserveAlreadySafeNames", "feat-oidc-dpop", "feat-oidc-dpop"},
{"ShouldPreserveDotsAndUnderscores", "fix/v4.38_backport", "fix-v4.38_backport"},
{"ShouldFoldWhitespaceAndOtherUnsafeRunes", "fix/oidc claims (#123)", "fix-oidc-claims-123-"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, sanitizeTag(tc.have))
})
}
}