diff --git a/.github/README.md b/.github/README.md index bc4cf3d..efb120e 100644 --- a/.github/README.md +++ b/.github/README.md @@ -10,6 +10,7 @@ GitHub Actions for working with Docker - [Docker lint](../README-lint.md) - [Docker build](../README-build.md) - [Docker push](../README-push.md) +- [Docker resolve-image](actions/resolve-image/README.md) ## Golden Path diff --git a/.github/actions/resolve-image/README.md b/.github/actions/resolve-image/README.md new file mode 100644 index 0000000..962a12f --- /dev/null +++ b/.github/actions/resolve-image/README.md @@ -0,0 +1,50 @@ +# `gha-docker/resolve-image` + +Resolves the Docker image that was built during a PR by looking up git tags created by the [push](../../../README-push.md) workflow. Intended for use in CD workflows that run after a PR is merged to `main`. + +> [!TIP] +> This is useful for only building the docker image once in the CI/CD pipelines. + +When the push workflow builds and pushes a Docker image, it also creates a git tag with the image tag value (e.g. `my-branch.20260409-SHA1234567`). This action finds the merged PR for the current commit, reconstructs the sanitised branch name, and looks up the most recent matching git tag to resolve the exact image that was built. + +> [!NOTE] +> If no matching tag is found (e.g. docs-only changes), the action succeeds with empty `image` and `image_tag` outputs. Downstream deploy steps can use this to skip deployment. + +## Usage + +Add the action as a step in your CD workflow. Requires checkout with full history and tags. + +```yml +jobs: + deploy: + if: github.event_name == 'push' + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - uses: entur/gha-docker/.github/actions/resolve-image@v1 + id: resolve-image + + - if: steps.resolve-image.outputs.image != '' + run: echo "Deploying ${{ steps.resolve-image.outputs.image }}" +``` + +## Inputs + +| INPUT | TYPE | REQUIRED | DEFAULT | DESCRIPTION | +| ------------ | ------ | -------- | ------------- | ------------------------------------------------------- | +| `image_name` | string | false | `"repo_name"` | Image name to resolve. Defaults to the repository name. | + +## Outputs + +| OUTPUT | DESCRIPTION | +| ----------- | -------------------------------------------------------------------------------------------- | +| `image` | Resolved image name and tag (`image_name:image_tag`), empty if no image was built for the PR | +| `image_tag` | Resolved image tag, empty if no image was built for the PR | +| `pr_number` | The PR number that was merged at the current commit | diff --git a/.github/actions/resolve-image/action.yml b/.github/actions/resolve-image/action.yml new file mode 100644 index 0000000..bd6f13b --- /dev/null +++ b/.github/actions/resolve-image/action.yml @@ -0,0 +1,68 @@ +name: Resolve Image +description: > + Resolve the Docker image name and tag for a merged PR by looking up + the git tag created by the Docker push workflow. Requires the repository + to be checked out with fetch-depth: 0 and fetch-tags: true. + +inputs: + image_name: + description: "Image name to resolve. Defaults to the repository name." + default: "repo_name" + +outputs: + image: + description: "Resolved image name and tag (image_name:image_tag), empty if no image was built for the PR" + value: ${{ steps.resolve.outputs.image }} + image_tag: + description: "Resolved image tag, empty if no image was built for the PR" + value: ${{ steps.resolve.outputs.image_tag }} + pr_number: + description: "The PR number that was merged at the current commit" + value: ${{ steps.find-pr.outputs.pr_number }} + +runs: + using: composite + steps: + - name: Find merged PR + id: find-pr + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + PR_NUMBER=$(gh pr list --search "$GITHUB_SHA" --state merged --json number --jq '.[0].number') + if [ -z "$PR_NUMBER" ]; then + echo "::error::Could not find merged PR for commit $GITHUB_SHA" + exit 1 + fi + echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" + + BRANCH=$(gh pr view "$PR_NUMBER" --json headRefName --jq '.headRefName') + echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT" + + - name: Sanitize branch name + id: sanitize + uses: entur/gha-docker/.github/actions/sanitize-ref@v1 + with: + ref: ${{ steps.find-pr.outputs.branch }} + + - name: Resolve image from git tag + id: resolve + shell: bash + env: + IMAGE_NAME: ${{ inputs.image_name }} + BRANCH: ${{ steps.sanitize.outputs.ref }} + PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }} + run: | + if [ "$IMAGE_NAME" = "repo_name" ]; then + IMAGE_NAME="${GITHUB_REPOSITORY#*/}" + fi + + IMAGE_TAG=$(git tag -l "${BRANCH}.*" --sort=-creatordate | head -1) + if [ -z "$IMAGE_TAG" ]; then + echo "::warning::No git tag matching '${BRANCH}.*' (PR #${PR_NUMBER}) — no image was built for this PR, skipping deploy" + exit 0 + fi + + echo "Resolved image: ${IMAGE_NAME}:${IMAGE_TAG} (from PR #${PR_NUMBER})" + echo "image=${IMAGE_NAME}:${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" diff --git a/.github/actions/sanitize-ref/README.md b/.github/actions/sanitize-ref/README.md new file mode 100644 index 0000000..eb0d9d8 --- /dev/null +++ b/.github/actions/sanitize-ref/README.md @@ -0,0 +1,22 @@ +# `gha-docker/sanitize-ref` + +Sanitizes a git ref name for use in Docker image tags. Used internally by the [push](../../../README-push.md) workflow and [resolve-image](../resolve-image/README.md) action to ensure consistent branch name handling. + +## Sanitization rules + +1. Truncate to 43 characters (reserves space for `.YYYYMMDD-SHA1234567` suffix) +2. Remove trailing `-` +3. Lowercase and remove Norwegian characters (`ÆØÅæøå`) +4. Replace `/`, `.`, `!` with `-` + +## Inputs + +| INPUT | TYPE | REQUIRED | DESCRIPTION | +| ----- | ------ | -------- | ------------------------ | +| `ref` | string | true | Git ref name to sanitize | + +## Outputs + +| OUTPUT | DESCRIPTION | +| ------ | ------------------ | +| `ref` | Sanitized ref name | diff --git a/.github/actions/sanitize-ref/action.yml b/.github/actions/sanitize-ref/action.yml new file mode 100644 index 0000000..49772f0 --- /dev/null +++ b/.github/actions/sanitize-ref/action.yml @@ -0,0 +1,33 @@ +name: Sanitize Ref +description: > + Sanitize a git ref name for use in Docker image tags. + Truncates to 43 chars, lowercases, removes Norwegian characters, + and replaces /, ., ! with hyphens. + +inputs: + ref: + description: "Git ref name to sanitize" + required: true + +outputs: + ref: + description: "Sanitized ref name" + value: ${{ steps.sanitize.outputs.ref }} + +runs: + using: composite + steps: + - name: Sanitize ref + id: sanitize + shell: bash + env: + RAW_REF: ${{ inputs.ref }} + run: | + REF="${RAW_REF}" + REF=${REF:0:43} # truncate to max_len - len(.YYYYMMDD-SHA1234567) + REF=$(echo "$REF" | sed 's/[-]$//') # remove trailing - + REF=$(echo "$REF" | tr '[:upper:]' '[:lower:]' | tr -d '[ÆØÅæøå]') # to ASCII lower case + REF=${REF//\//-} # replace / with - + REF=${REF//./-} # replace . with - + REF=${REF//!/-} # replace ! with - + echo "ref=${REF}" >> "$GITHUB_OUTPUT" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 671fd36..a5bf2cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,7 @@ updates: - ".github/workflows/build.yml" - ".github/workflows/lint.yml" - ".github/workflows/push.yml" + - ".github/actions" groups: minor-and-patch: applies-to: version-updates @@ -25,6 +26,7 @@ updates: - "/.github/workflows/build.yml" - "/.github/workflows/lint.yml" - "/.github/workflows/push.yml" + - "/.github/actions/resolve-image" groups: minor-and-patch: applies-to: version-updates diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 6885e19..c478592 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -84,19 +84,24 @@ jobs: shell: bash run: | echo "GHA_DOCKER_PUSH_DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV - # Convert ref name to a valid git & docker tag name if [[ "${GITHUB_EVENT_NAME}" = "pull_request" ]]; then - BRANCH_NAME=${GITHUB_HEAD_REF} + echo "ref=${GITHUB_HEAD_REF}" >> "$GITHUB_OUTPUT" else - BRANCH_NAME=${GITHUB_REF_NAME} + echo "ref=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" fi - BRANCH_NAME=${BRANCH_NAME:0:43} # truncate to max_len - len(.YYYYMMDD-SHA1234567) - BRANCH_NAME=$(echo "$BRANCH_NAME" | sed s'/[-]$//') # remove trailing - - BRANCH_NAME=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | tr -d '[ÆØÅæøå]') # to ASCII lower case - BRANCH_NAME=${BRANCH_NAME//\//-} # replace / with - - BRANCH_NAME=${BRANCH_NAME//./-} # replace . with - - BRANCH_NAME=${BRANCH_NAME//!/-} # replace ! with - - echo "GHA_DOCKER_PUSH_BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV + + - name: Sanitize branch name + id: sanitize-branch + uses: entur/gha-docker/.github/actions/sanitize-ref@v1 + with: + ref: ${{ steps.set-env.outputs.ref }} + + - name: Set sanitized branch name + id: set-branch + shell: bash + env: + SANITIZED_REF: ${{ steps.sanitize-branch.outputs.ref }} + run: echo "GHA_DOCKER_PUSH_BRANCH_NAME=${SANITIZED_REF}" >> $GITHUB_ENV - if: env.GHA_DOCKER_PUSH_IMAGE_NAME == 'repo_name' name: Set image artifact name