Skip to content

Commit 4428d01

Browse files
authored
fix: Fix workflows with release tagging and semver support (#8)
1 parent a0cfad1 commit 4428d01

4 files changed

Lines changed: 105 additions & 35 deletions

File tree

.github/workflows/semantic-release.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ on:
1212
release-created:
1313
description: 'Whether semantic-release created a new release/tag'
1414
value: ${{ jobs.release.outputs.release-created }}
15+
release-tag:
16+
description: 'The tag created by semantic-release (empty if no release was created)'
17+
value: ${{ jobs.release.outputs.release-tag }}
1518

1619
permissions:
1720
contents: write
@@ -21,16 +24,21 @@ permissions:
2124
jobs:
2225
release:
2326
runs-on: ubuntu-latest
27+
# Prevent concurrent releases from the same repository. A queued run will wait
28+
# rather than being cancelled, ensuring no release commit is ever skipped.
2429
concurrency:
2530
group: semantic-release-${{ github.repository }}
2631
cancel-in-progress: false
2732
outputs:
2833
release-created: ${{ steps.release-check.outputs.release-created }}
34+
release-tag: ${{ steps.release-check.outputs.release-tag }}
2935

3036
steps:
3137
- name: Checkout code
3238
uses: actions/checkout@v4
3339
with:
40+
# Full history is required — semantic-release reads all commits since the last
41+
# tag to determine the next version and generate the changelog.
3442
fetch-depth: 0
3543

3644
- name: Set up Node.js
@@ -47,6 +55,8 @@ jobs:
4755
@semantic-release/changelog \
4856
@semantic-release/github
4957
58+
# Snapshot the current tags before running semantic-release so we can diff
59+
# afterwards to find exactly which tag (if any) was newly created.
5060
- name: Capture tags before release
5161
run: |
5262
git fetch --tags --force
@@ -62,20 +72,39 @@ jobs:
6272
git fetch --tags --force
6373
git tag -l | sort > /tmp/tags-after.txt
6474
75+
# semantic-release does not reliably expose whether it created a release via exit
76+
# codes or stdout — behaviour varies across plugin configurations. Instead we diff
77+
# the tag snapshots taken before and after, then check whether any new tag points
78+
# at HEAD. This approach works regardless of the release.config.js in the caller repo.
6579
- name: Determine whether release was created
6680
id: release-check
6781
run: |
6882
head_sha=$(git rev-parse HEAD)
83+
84+
# comm -13 outputs lines present in the second file but not the first,
85+
# i.e. tags that did not exist before semantic-release ran.
6986
new_tags=$(comm -13 /tmp/tags-before.txt /tmp/tags-after.txt)
7087
release_created=false
88+
release_tag=""
89+
90+
# Short-circuit if no new tags were created at all.
91+
if [ -z "$new_tags" ]; then
92+
echo "release-created=false" >> "$GITHUB_OUTPUT"
93+
echo "release-tag=" >> "$GITHUB_OUTPUT"
94+
exit 0
95+
fi
7196
7297
while IFS= read -r tag; do
98+
# Lightweight tags point directly to a commit; annotated tags point to a tag
99+
# object which in turn points to a commit. The '^{}' suffix dereferences an
100+
# annotated tag to its underlying commit SHA so both cases are handled uniformly.
73101
tag_sha=$(git rev-parse "${tag}^{}" 2>/dev/null || git rev-parse "$tag")
74102
if [ "$tag_sha" = "$head_sha" ]; then
75103
release_created=true
104+
release_tag="$tag"
76105
break
77106
fi
78107
done <<< "$new_tags"
79108
80109
echo "release-created=$release_created" >> "$GITHUB_OUTPUT"
81-
110+
echo "release-tag=$release_tag" >> "$GITHUB_OUTPUT"

.github/workflows/semver-container.yml

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ on:
1212
required: false
1313
default: 'ghcr.io'
1414
type: string
15+
tag:
16+
description: 'The release tag to derive semver from (e.g. 1.2.3 or v1.2.3)'
17+
required: true
18+
type: string
1519

1620
env:
1721
image-name: ${{ inputs.image-name }}
@@ -22,47 +26,74 @@ jobs:
2226
runs-on: ubuntu-latest
2327
permissions:
2428
packages: write # container images
25-
contents: write # releases
2629
steps:
27-
- name: Check out the repo
28-
uses: actions/checkout@v4
29-
30-
# some docker actions need all lowercase
30+
# docker/metadata-action and akhilerm/tag-push-action require lowercase registry paths.
31+
# GITHUB_REPOSITORY_OWNER can contain uppercase letters (e.g. "Health-Informatics-UoN"),
32+
# so we normalise it here and store it in GITHUB_ENV for all subsequent steps.
3133
- name: downcase repo-owner
3234
run: |
3335
echo "REPO_OWNER_LOWER=${GITHUB_REPOSITORY_OWNER,,}" >>${GITHUB_ENV}
3436
3537
- name: Parse version from tag
3638
id: version
37-
uses: release-kit/semver@97491c46500b6e758ced599794164a234b8aa08c # v2.0.7
39+
run: |
40+
# The tag input may optionally be prefixed with 'v' (e.g. v1.2.3).
41+
# Strip it so all subsequent steps work with a bare semver string.
42+
version="${{ inputs.tag }}"
43+
version="${version#v}"
44+
45+
# Extract the major and minor components for floating tags (e.g. '1' and '1.2').
46+
# Floating tags let consumers pin to a major or minor version without knowing
47+
# the exact patch, following standard container image tagging conventions.
48+
major=$(echo "$version" | cut -d. -f1)
49+
minor=$(echo "$version" | cut -d. -f2)
50+
echo "version=$version" >> "$GITHUB_OUTPUT"
51+
echo "major=$major" >> "$GITHUB_OUTPUT"
52+
echo "major-minor=$major.$minor" >> "$GITHUB_OUTPUT"
53+
54+
# A hyphen in the version string indicates a pre-release (e.g. 1.0.0-beta.1).
55+
# Floating major/minor tags must NOT be applied to pre-releases — those tags
56+
# should always point at the latest stable release, not a pre-release build.
57+
if [[ "$version" == *-* ]]; then
58+
echo "is-prerelease=true" >> "$GITHUB_OUTPUT"
59+
else
60+
echo "is-prerelease=false" >> "$GITHUB_OUTPUT"
61+
fi
3862
39-
# check image exists for commit
40-
- uses: tyriis/docker-image-tag-exists@71a750a41aa78e4efb0842f538140c5df5b8166f # v2.1.0
63+
# Verify that the edge image built from this commit SHA actually exists in the registry
64+
# before attempting to retag it. Fails fast rather than producing a misleading error
65+
# from the tag-push step if the publish-container workflow hasn't run yet.
66+
- name: Check image exists for commit
67+
uses: tyriis/docker-image-tag-exists@71a750a41aa78e4efb0842f538140c5df5b8166f # v2.1.0
4168
with:
4269
registry: ${{ env.registry }}
4370
repository: ${{ env.REPO_OWNER_LOWER }}/${{ env.image-name }}
4471
tag: ${{ github.sha }}
4572

46-
# standard login to the container registry
4773
- name: Docker Login
4874
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
4975
with:
5076
registry: ${{ env.registry }}
5177
username: ${{github.actor}}
5278
password: ${{secrets.GITHUB_TOKEN}}
5379

54-
# We still use the metadata action to help build out our tags from the Workflow Run
80+
# Build the list of tags to apply. We use type=raw because the version comes from
81+
# the 'tag' input rather than from GITHUB_REF — this workflow is triggered via
82+
# workflow_call on a branch push, so GITHUB_REF is a branch ref, not a tag ref.
83+
# The major and minor floating tags are gated on is-prerelease so they are only
84+
# moved forward for stable releases.
5585
- name: Docker Metadata action
5686
id: meta
5787
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
5888
with:
5989
images: ${{ env.registry }}/${{ env.REPO_OWNER_LOWER }}/${{ env.image-name }}
60-
tags: | # new tags only
61-
type=semver,pattern={{version}}
62-
type=semver,pattern={{major}}
63-
type=semver,pattern={{major}}.{{minor}}
90+
tags: |
91+
type=raw,value=${{ steps.version.outputs.version }}
92+
type=raw,value=${{ steps.version.outputs.major }},enable=${{ steps.version.outputs.is-prerelease == 'false' }}
93+
type=raw,value=${{ steps.version.outputs.major-minor }},enable=${{ steps.version.outputs.is-prerelease == 'false' }}
6494
65-
# apply the new tags to the existing images
95+
# Rather than rebuilding the image, we retag the existing edge image (tagged with
96+
# the commit SHA by publish-container) with the semver tags computed above.
6697
- name: Push updated image tags
6798
uses: akhilerm/tag-push-action@f35ff2cb99d407368b5c727adbcc14a2ed81d509 # v2.2.0
6899
with:

docs/semantic-release.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ Reusable workflow that runs [semantic-release](https://github.com/semantic-relea
1010

1111
## Outputs
1212

13-
| Output | Description |
14-
|-------------------|----------------------------------------------------------|
15-
| `release-created` | `'true'` if a new release was created, `'false'` if not. |
13+
| Output | Description |
14+
|-------------------|--------------------------------------------------------------------------|
15+
| `release-created` | `'true'` if a new release was created, `'false'` if not. |
16+
| `release-tag` | The tag created by semantic-release (e.g. `1.2.3`). Empty string if no release was created. |
1617

17-
Use this to conditionally run downstream jobs (e.g. re-tag a container, publish to PyPI) only when a release actually happened:
18+
Use these to conditionally run downstream jobs (e.g. re-tag a container, publish to PyPI) only when a release actually happened:
1819

1920
```yaml
2021
jobs:
@@ -25,7 +26,11 @@ jobs:
2526
publish:
2627
needs: release
2728
if: needs.release.outputs.release-created == 'true'
28-
...
29+
uses: health-informatics-uon/workflows/.github/workflows/semver-container.yml@main
30+
with:
31+
image-name: my-app
32+
tag: ${{ needs.release.outputs.release-tag }}
33+
secrets: inherit
2934
```
3035
3136
> **Note:** If the workflow itself errors, `release-created` will be an empty string rather than `'false'`. Always use `== 'true'` rather than `!= 'false'` in conditions.

docs/semver-container.md

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,46 @@
22

33
Reusable workflow that:
44

5-
- tags an existing container image with semver tags (`x.y.z`, `x`, `x.y`) based on the git tag
5+
- tags an existing container image with semver tags (`x.y.z`, `x`, `x.y`) based on a provided release tag
66
- verifies that a corresponding container image (tagged with the commit SHA) already exists
77

8-
Use this when you already have an \"edge\" or commit-SHA container image (for example from the `publish-container` workflow) and you want to promote it to a semver release.
8+
Use this when you already have an "edge" or commit-SHA container image (for example from the `publish-container` workflow) and you want to promote it to a semver release.
99

10-
## Inputs
10+
> **Note:** The `x` and `x.y` floating tags are only applied for stable releases. Pre-release versions (e.g. `1.0.0-beta.1`) receive only the full version tag.
1111
12-
| Input | Required | Default | Description |
13-
|---------------------|----------|------------------------------------|------------------------------------------------------------------|
14-
| `image-name` | Yes || Image name, e.g. `my-app` or `services/api`. |
15-
| `registry` | No | `'ghcr.io'` | Registry host. |
16-
| `release-name-prefix` | No | `''` | String prefixed to the Release title (e.g. project name + space).|
12+
## Inputs
1713

14+
| Input | Required | Default | Description |
15+
|--------------|----------|-------------|------------------------------------------------------|
16+
| `image-name` | Yes || Image name, e.g. `my-app` or `services/api`. |
17+
| `registry` | No | `'ghcr.io'` | Registry host. |
18+
| `tag` | Yes || The release tag to derive semver from (e.g. `1.2.3` or `v1.2.3`). Typically passed from the `release-tag` output of the `semantic-release` workflow. |
1819

1920
## Secrets
2021

21-
- `GITHUB_TOKEN` — Pass with `secrets: inherit` so the workflow can read tags, create releases, and push tags in the container registry.
22+
- `GITHUB_TOKEN` — Pass with `secrets: inherit` so the workflow can push tags in the container registry.
2223

2324
## Usage
2425

25-
This workflow is designed to as part of a release process.
26+
This workflow is designed to run as part of a release process, chained after `semantic-release`:
2627

2728
```yaml
2829
jobs:
30+
release:
31+
uses: health-informatics-uon/workflows/.github/workflows/semantic-release.yml@main
32+
secrets: inherit
33+
2934
semver-container:
35+
needs: release
36+
if: needs.release.outputs.release-created == 'true'
3037
uses: health-informatics-uon/workflows/.github/workflows/semver-container.yml@main
3138
with:
3239
image-name: my-service
33-
registry: ghcr.io
34-
release-name-prefix: 'My Service '
40+
tag: ${{ needs.release.outputs.release-tag }}
3541
secrets: inherit
3642
```
3743
3844
This will:
3945
4046
- check that an image `ghcr.io/<owner>/my-service:<sha>` exists
41-
- push additional tags like `1.2.3`, `1`, and `1.2` to that image
42-
47+
- push additional tags like `1.2.3`, `1`, and `1.2` to that image (or just `1.2.3` for pre-releases)

0 commit comments

Comments
 (0)