diff --git a/README.md b/README.md index 780ca4c..5f1a17b 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Whenever the action updates a GitOps file, it stamps the following annotations o |------------|-------| | `deploy.staffbase.com/repositoryFullName` | The source repository in `owner/repo` form (`$GITHUB_REPOSITORY`) | | `deploy.staffbase.com/commitSha` | The commit SHA being deployed (`$GITHUB_SHA`) | -| `deploy.staffbase.com/version` | The deployed image tag — `dev-` on `dev`, `main-` on `main`/`master`, the tag name on tag pushes | +| `deploy.staffbase.com/version` | The deployed image tag — `dev-` on `dev`, `main-` on `main`, `master-` on `master` (with `docker-tag-timestamp` a UTC timestamp is inserted before the SHA), the version without the leading `v` on `v*` tag pushes, and the tag name on other tag pushes | These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings/organization/configuring-deployments-in-swarmia) field names and are read by `flux-deployment-reporter` to report deployments to Swarmia once Flux finishes reconciling. @@ -126,6 +126,7 @@ These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings | `docker-registry-api` | Docker Registry API (used for retagging without pulling) | `https://registry.staffbase.com/v2/` | | `docker-image` | Docker Image | | | `docker-custom-tag` | Docker Custom Tag to be set on the image | | +| `docker-tag-timestamp` | Insert a UTC timestamp into `dev`/`main`/`master` branch tags (`dev--`) to make them sortable for Flux image automation | `false` | | `docker-username` | Username for the Docker Registry | | | `docker-password` | Password for the Docker Registry | | | `docker-file` | Dockerfile | `./Dockerfile` | @@ -153,6 +154,49 @@ These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings | `docker-digest` | Digest of the image | | `docker-tag` | Tag of the image | +## Image tags & Flux image automation + +The generated image tag depends on the Git ref: + +| Ref | Tag (default) | Tag (`docker-tag-timestamp: 'true'`) | Floating tag | +|-----|---------------|--------------------------------------|--------------| +| `dev` branch | `dev-` | `dev--` | `dev` | +| `main` branch | `main-` | `main--` | `main` | +| `master` branch | `master-` | `master--` | `master` | +| `v*` tag (prod) | the version, e.g. `2025.50.14` (CalVer) | _(unchanged)_ | `latest` | +| other branch | `` (not pushed) | _(unchanged)_ | — | + +By default branch tags keep the legacy `-` shape. Set +`docker-tag-timestamp: 'true'` to insert a `YYYYMMDDHHMMSS` (UTC) timestamp before +the SHA. This makes branch tags **sortable** so +[Flux image automation](https://fluxcd.io/flux/components/image/) can pick the +newest build — the Git SHA alone is not orderable. The short SHA is kept for +traceability and Flux sorts on the timestamp only. + +With the timestamp enabled, use one `ImagePolicy` per environment, filtering by prefix: + +```yaml +# dev (and likewise main-/master- for stage) +spec: + imageRepositoryRef: { name: my-service } + filterTags: + pattern: '^dev-(?P[0-9]+)-[0-9a-f]+$' + extract: '$ts' + policy: + numerical: { order: asc } +--- +# prod — CalVer tags parse as SemVer (no zero-padding!) +spec: + imageRepositoryRef: { name: my-service } + policy: + semver: { range: '>=0.0.0' } +``` + +> **Note:** the prod `semver` policy only works if CalVer parts are never +> zero-padded (`2025.5.3`, not `2025.05.03`) — SemVer forbids leading zeros. +> Track the immutable `*--` tags, not the floating `dev`/`main` +> tags, so deployments keep their provenance. + ## Contributing Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull diff --git a/action.yml b/action.yml index aa1954e..2950518 100644 --- a/action.yml +++ b/action.yml @@ -17,6 +17,10 @@ inputs: docker-custom-tag: description: 'Docker Custom Tag' required: false + docker-tag-timestamp: + description: 'Insert a UTC timestamp into dev/main/master branch tags (e.g. dev-20260602143055-) to make them sortable for Flux image automation. Opt-in; defaults to the legacy - format.' + required: false + default: 'false' docker-username: description: 'Username for the Docker Registry' required: false @@ -112,6 +116,7 @@ runs: shell: bash env: INPUT_DOCKER_CUSTOM_TAG: ${{ inputs.docker-custom-tag }} + INPUT_DOCKER_TAG_TIMESTAMP: ${{ inputs.docker-tag-timestamp }} INPUT_DOCKER_DISABLE_RETAGGING: ${{ inputs.docker-disable-retagging }} INPUT_DOCKER_REGISTRY: ${{ inputs.docker-registry }} INPUT_DOCKER_IMAGE: ${{ inputs.docker-image }} diff --git a/scripts/generate-tags.sh b/scripts/generate-tags.sh index f804548..8f5b26d 100755 --- a/scripts/generate-tags.sh +++ b/scripts/generate-tags.sh @@ -17,21 +17,41 @@ require_env INPUT_DOCKER_IMAGE BUILD="true" +# branch_tag builds the immutable tag for an environment branch. +# +# When INPUT_DOCKER_TAG_TIMESTAMP is "true" the tag gets a UTC timestamp inserted +# before the short SHA (e.g. dev-20260602143055-abcdef12). This makes branch tags +# sortable by Flux image automation (numerical policy) — the git SHA alone is not +# orderable, so Flux cannot otherwise tell which build is newest. The SHA is kept +# for traceability. When the flag is unset/false the legacy - shape +# is produced, so existing consumers are unaffected. +# +# The timestamp is overridable via BUILD_TIMESTAMP for deterministic tests. +branch_tag() { + local prefix="$1" + if [[ "${INPUT_DOCKER_TAG_TIMESTAMP:-false}" == "true" ]]; then + local ts="${BUILD_TIMESTAMP:-$(date -u +%Y%m%d%H%M%S)}" + echo "${prefix}-${ts}-${GITHUB_SHA::8}" + else + echo "${prefix}-${GITHUB_SHA::8}" + fi +} + if [[ -n "${INPUT_DOCKER_CUSTOM_TAG:-}" ]]; then TAG="${INPUT_DOCKER_CUSTOM_TAG}" LATEST="latest" PUSH="true" BUILD="${INPUT_DOCKER_DISABLE_RETAGGING:-false}" elif [[ $GITHUB_REF == refs/heads/master ]]; then - TAG="master-${GITHUB_SHA::8}" + TAG="$(branch_tag master)" LATEST="master" PUSH="true" elif [[ $GITHUB_REF == refs/heads/main ]]; then - TAG="main-${GITHUB_SHA::8}" + TAG="$(branch_tag main)" LATEST="main" PUSH="true" elif [[ $GITHUB_REF == refs/heads/dev ]]; then - TAG="dev-${GITHUB_SHA::8}" + TAG="$(branch_tag dev)" LATEST="dev" PUSH="true" elif [[ $GITHUB_REF == refs/tags/v* ]]; then diff --git a/tests/generate-tags.bats b/tests/generate-tags.bats index 27323c0..2fdfc92 100644 --- a/tests/generate-tags.bats +++ b/tests/generate-tags.bats @@ -11,6 +11,11 @@ setup() { export INPUT_DOCKER_IMAGE="my-service" export INPUT_DOCKER_CUSTOM_TAG="" export INPUT_DOCKER_DISABLE_RETAGGING="false" + # Timestamp suffix is opt-in; default off so the legacy - format + # is the baseline. Tests that exercise the suffix set the flag explicitly. + unset INPUT_DOCKER_TAG_TIMESTAMP + # Pin the timestamp so the opt-in branch tags are deterministic in tests. + export BUILD_TIMESTAMP="20260602143055" } teardown() { @@ -53,6 +58,59 @@ teardown() { assert_output_value "build" "true" } +# --- timestamp suffix (opt-in, Flux-sortable) --- + +@test "dev branch with timestamp flag inserts timestamp before sha" { + export INPUT_DOCKER_TAG_TIMESTAMP="true" + export GITHUB_REF="refs/heads/dev" + run "$SCRIPT" + assert_success + assert_output_value "tag" "dev-20260602143055-abcdef12" + assert_output_value "latest" "dev" +} + +@test "main branch with timestamp flag inserts timestamp before sha" { + export INPUT_DOCKER_TAG_TIMESTAMP="true" + export GITHUB_REF="refs/heads/main" + run "$SCRIPT" + assert_success + assert_output_value "tag" "main-20260602143055-abcdef12" +} + +@test "master branch with timestamp flag inserts timestamp before sha" { + export INPUT_DOCKER_TAG_TIMESTAMP="true" + export GITHUB_REF="refs/heads/master" + run "$SCRIPT" + assert_success + assert_output_value "tag" "master-20260602143055-abcdef12" +} + +@test "timestamp flag defaults to a 14-digit UTC timestamp when not injected" { + export INPUT_DOCKER_TAG_TIMESTAMP="true" + unset BUILD_TIMESTAMP + export GITHUB_REF="refs/heads/dev" + run "$SCRIPT" + assert_success + local tag + tag=$(get_output_value "tag") + [[ "$tag" =~ ^dev-[0-9]{14}-abcdef12$ ]] +} + +@test "timestamp flag does not affect version (prod) tags" { + export INPUT_DOCKER_TAG_TIMESTAMP="true" + export GITHUB_REF="refs/tags/v2025.50.14" + run "$SCRIPT" + assert_success + assert_output_value "tag" "2025.50.14" +} + +@test "timestamp flag explicitly 'false' produces legacy shape" { + export INPUT_DOCKER_TAG_TIMESTAMP="false" + export GITHUB_REF="refs/heads/dev" + run "$SCRIPT" + assert_success + assert_output_value "tag" "dev-abcdef12" +} # --- version tag --- @test "version tag v1.2.3 generates correct tag" {