Skip to content

Commit 84006f6

Browse files
authored
Merge pull request #3976 from esune/feat/semantic-image-versioning
Feature: semantic image versioning for released images
2 parents 710a2f9 + d813635 commit 84006f6

6 files changed

Lines changed: 359 additions & 32 deletions

File tree

.github/LTS-README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# LTS Version Configuration
2+
3+
This file controls which version patterns are treated as Long Term Support (LTS) releases.
4+
5+
## How it works
6+
7+
When a release is published, the LTS workflow automatically:
8+
1. Checks if the release version (major.minor) is listed in this file
9+
2. If it matches, creates an LTS tag and GitHub release
10+
3. Tags the published container images with the LTS tag
11+
12+
## Format
13+
14+
- One version pattern per line in `major.minor` format (e.g., `1.2` for versions 1.2.x)
15+
- Lines starting with `#` are comments and will be ignored
16+
- Empty lines are ignored
17+
18+
## Example
19+
20+
To enable LTS for versions 0.11.x and 1.0.x, add:
21+
```
22+
0.11
23+
1.0
24+
```
25+
26+
## Adding a new LTS version
27+
28+
1. Edit `.github/lts-versions.txt`
29+
2. Add the major.minor version pattern (e.g., `1.3`)
30+
3. Commit and push the changes
31+
4. Future releases matching that pattern (e.g., `1.3.0`, `1.3.1`, etc.) will automatically be tagged as LTS
32+
33+
## Behavior
34+
35+
- For release `1.2.3` with `1.2` in this file:
36+
- Creates git tag: `1.2-lts`
37+
- Creates GitHub release: `1.2-lts`
38+
- Tags images: `py3.12-1.2-lts`
39+
40+
- When `1.2.4` is released:
41+
- Moves `1.2-lts` tag to point to `1.2.4`
42+
- Updates the `1.2-lts` GitHub release
43+
- Re-tags images so `py3.12-1.2-lts` points to the `1.2.4` image
44+
45+
This ensures the LTS tag always points to the latest patch release for that major.minor version.

.github/lts-versions.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# LTS Version Patterns
2+
# Each line represents a version pattern that should be treated as LTS
3+
# Use major.minor format (e.g., 1.2 for versions 1.2.x)
4+
# Lines starting with # are comments and will be ignored
5+
# Empty lines are ignored
6+
7+
# Example: Uncomment the lines below to enable LTS for specific versions
8+
0.12
9+
1.2
10+
1.3
11+
12+
# For testing purposes (remove in production):
13+
# 0.0

.github/workflows/publish.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,35 @@ jobs:
125125
id: lower
126126
run: echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT
127127

128+
- name: Check if Highest Semantic Version
129+
id: check_latest
130+
env:
131+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
132+
CURRENT_TAG: ${{ inputs.tag || github.event.release.tag_name }}
133+
run: |
134+
# Skip if this is an RC release
135+
if [[ "$CURRENT_TAG" =~ rc ]]; then
136+
echo "is_latest=false" >> "$GITHUB_OUTPUT"
137+
echo "Skipping latest tag for RC release"
138+
exit 0
139+
fi
140+
141+
# Get all non-RC release tags and sort by semantic version
142+
HIGHEST_TAG=$(gh api /repos/${{ github.repository }}/releases --paginate \
143+
| jq -r '[.[] | select(.prerelease == false and (.tag_name | test("rc") | not)) | .tag_name] | .[]' \
144+
| sort -V | tail -1)
145+
146+
echo "Current tag: $CURRENT_TAG"
147+
echo "Highest semantic version: $HIGHEST_TAG"
148+
149+
if [ "$CURRENT_TAG" == "$HIGHEST_TAG" ]; then
150+
echo "is_latest=true" >> "$GITHUB_OUTPUT"
151+
echo "This is the highest semantic version - will tag as latest"
152+
else
153+
echo "is_latest=false" >> "$GITHUB_OUTPUT"
154+
echo "Not the highest semantic version - skipping latest tag"
155+
fi
156+
128157
- name: Setup Image Metadata
129158
id: meta
130159
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
@@ -133,6 +162,9 @@ jobs:
133162
ghcr.io/${{ steps.lower.outputs.owner }}/${{ matrix.image-name }}
134163
tags: |
135164
type=raw,value=py${{ matrix.python-version }}-${{ inputs.tag || github.event.release.tag_name }}
165+
type=semver,pattern={{version}},value=${{ inputs.tag || github.event.release.tag_name }}
166+
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag || github.event.release.tag_name }},enable=${{ !contains(github.event.release.tag_name || inputs.tag, 'rc') }}
167+
type=raw,value=latest,enable=${{ steps.check_latest.outputs.is_latest == 'true' }}
136168
137169
- name: Publish Image to GHCR.io
138170
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
Lines changed: 207 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,202 @@
11
# This Action will run when a release is published from the LTS branches
2-
# and create new LTS tag, release and publish the image in GHCR
2+
# and create new LTS tag, release and tag the existing image in GHCR
33

44
name: Tag and Recreate LTS Release
55

66
on:
7-
release:
8-
types: [published]
7+
workflow_run:
8+
workflows: ["Publish ACA-Py Image"]
9+
types: [completed]
10+
workflow_dispatch:
11+
inputs:
12+
release_tag:
13+
description: 'Release tag to create LTS from (e.g., 1.2.3)'
14+
required: true
15+
type: string
916

1017
permissions:
1118
contents: write
1219
packages: write
1320

1421
jobs:
1522
recreate-lts-release:
16-
# This job is disabled by default for main, should be enabled for LTS branches and tags.
17-
# To enable it, you can set the condition in the `if` statement below.
18-
# The condition should check if the release tag starts with the LTS version prefix.
19-
# For example, if your LTS versions are prefixed with '1.2.', you can use:
20-
# if: startsWith(github.event.release.tag_name, '1.2.')
21-
# This will ensure that the job only runs for releases that are tagged with LTS versions.
22-
if: false
23+
# LTS versions are now configured in .github/lts-versions.txt
24+
# Add version patterns (major.minor format) to that file to enable LTS processing
25+
if: |
26+
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') ||
27+
(github.event_name == 'workflow_dispatch')
2328
name: Recreate LTS Release
2429
runs-on: ubuntu-latest
2530
outputs:
26-
lts_tag: ${{ steps.vars.outputs.LTS_TAG }}
31+
lts_tag: ${{ steps.set_outputs.outputs.lts_tag }}
32+
release_tag: ${{ steps.set_outputs.outputs.release_tag }}
2733

2834
steps:
2935
- name: Checkout repository
3036
uses: actions/checkout@v6
3137
with:
3238
fetch-depth: 0
3339

40+
- name: Get Release Tag
41+
id: get_release
42+
if: github.event_name == 'workflow_run'
43+
env:
44+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
run: |
46+
SHA="${{ github.event.workflow_run.head_sha }}"
47+
echo "Looking for release associated with commit: $SHA"
48+
49+
# Try to find release by target_commitish first (fast path)
50+
RELEASE_TAG=$(gh api /repos/${{ github.repository }}/releases --paginate | \
51+
jq -r --arg sha "$SHA" \
52+
'.[] | select(.target_commitish == $sha) | .tag_name' | head -1)
53+
54+
# If not found, try to find tags pointing to this commit and check for releases
55+
if [ -z "$RELEASE_TAG" ]; then
56+
echo "No release found by target_commitish, checking tags pointing to commit..."
57+
TAGS=$(git tag --points-at "$SHA" 2>/dev/null || echo "")
58+
59+
if [ -n "$TAGS" ]; then
60+
echo "Tags found: $TAGS"
61+
# Fetch all releases once
62+
ALL_RELEASES=$(gh api /repos/${{ github.repository }}/releases --paginate | jq -r '.[].tag_name')
63+
64+
# Check each tag to see if it has a release
65+
for TAG in $TAGS; do
66+
if echo "$ALL_RELEASES" | grep -q "^${TAG}$"; then
67+
RELEASE_TAG="$TAG"
68+
echo "Found release for tag: $TAG"
69+
break
70+
fi
71+
done
72+
fi
73+
fi
74+
75+
if [ -z "$RELEASE_TAG" ]; then
76+
echo "No release found for commit $SHA"
77+
echo "skip=true" >> "$GITHUB_OUTPUT"
78+
elif [[ "$RELEASE_TAG" =~ rc ]]; then
79+
echo "Release $RELEASE_TAG is an RC release - skipping LTS tagging"
80+
echo "skip=true" >> "$GITHUB_OUTPUT"
81+
else
82+
echo "Found release: $RELEASE_TAG"
83+
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
84+
echo "skip=false" >> "$GITHUB_OUTPUT"
85+
fi
86+
87+
- name: Check if Release is LTS
88+
id: check_lts
89+
if: steps.get_release.outputs.skip != 'true' || github.event_name == 'workflow_dispatch'
90+
run: |
91+
# Get release tag based on trigger type
92+
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
93+
RELEASE_TAG="${{ inputs.release_tag }}"
94+
else
95+
RELEASE_TAG="${{ steps.get_release.outputs.release_tag }}"
96+
fi
97+
98+
# Read LTS versions from config file (remove comments and empty lines)
99+
LTS_VERSIONS=$(grep -v '^#' .github/lts-versions.txt | grep -v '^$' | tr '\n' '|' | sed 's/|$//')
100+
101+
if [ -z "$LTS_VERSIONS" ]; then
102+
echo "No LTS versions configured in .github/lts-versions.txt"
103+
echo "skip=true" >> "$GITHUB_OUTPUT"
104+
exit 0
105+
fi
106+
107+
echo "Configured LTS versions: $LTS_VERSIONS"
108+
echo "Checking release: $RELEASE_TAG"
109+
110+
# Extract major.minor from release tag
111+
SHORT_TAG=$(echo "$RELEASE_TAG" | cut -d. -f1,2)
112+
113+
# Check if it matches any LTS version pattern
114+
if echo "$SHORT_TAG" | grep -qE "^($LTS_VERSIONS)$"; then
115+
echo "Release $RELEASE_TAG matches LTS version $SHORT_TAG"
116+
echo "skip=false" >> "$GITHUB_OUTPUT"
117+
else
118+
echo "Release $RELEASE_TAG (version $SHORT_TAG) is not configured as LTS"
119+
echo "skip=true" >> "$GITHUB_OUTPUT"
120+
fi
121+
122+
- name: Set Release Tag for Output
123+
id: set_release_tag
124+
run: |
125+
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
126+
RELEASE_TAG="${{ inputs.release_tag }}"
127+
else
128+
RELEASE_TAG="${{ steps.get_release.outputs.release_tag }}"
129+
fi
130+
131+
# Validate semantic versioning format (semver 2.0)
132+
if ! echo "$RELEASE_TAG" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
133+
echo "Error: Release tag '$RELEASE_TAG' does not match semantic versioning format (X.Y.Z[-prerelease][+build])"
134+
exit 1
135+
fi
136+
137+
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
138+
echo "Validated release tag: $RELEASE_TAG"
139+
140+
- name: Determine if workflow should proceed
141+
id: should_proceed
142+
run: |
143+
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
144+
# workflow_dispatch: check LTS validation result
145+
if [ "${{ steps.check_lts.outputs.skip }}" == "true" ]; then
146+
echo "proceed=false" >> "$GITHUB_OUTPUT"
147+
echo "Skipping: Release is not configured as LTS"
148+
else
149+
echo "proceed=true" >> "$GITHUB_OUTPUT"
150+
echo "Proceeding with workflow_dispatch"
151+
fi
152+
elif [ "${{ steps.get_release.outputs.skip }}" == "true" ] || [ "${{ steps.check_lts.outputs.skip }}" == "true" ]; then
153+
# workflow_run: skip if either check failed
154+
echo "proceed=false" >> "$GITHUB_OUTPUT"
155+
echo "Skipping: release check or LTS check failed"
156+
else
157+
# workflow_run: proceed if both checks passed
158+
echo "proceed=true" >> "$GITHUB_OUTPUT"
159+
echo "Proceeding with workflow_run"
160+
fi
161+
34162
- name: Set up Git identity
163+
if: steps.should_proceed.outputs.proceed == 'true'
35164
run: |
36165
git config user.name "github-actions"
37166
git config user.email "github-actions@github.com"
38167
39168
- name: Determine LTS tag and update
169+
if: steps.should_proceed.outputs.proceed == 'true'
40170
id: vars
41171
env:
42-
BRANCH_REF: ${{ github.event.release.target_commitish }}
43-
RELEASE_TAG: ${{ github.event.release.tag_name }}
44-
RELEASE_BODY: ${{ github.event.release.body }}
172+
RELEASE_TAG: ${{ steps.set_release_tag.outputs.release_tag }}
173+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45174
run: |
46-
echo "Release published from branch: $BRANCH_REF"
175+
echo "Processing release: $RELEASE_TAG"
47176
48-
# Creating a LTS tag from the branch name
177+
# Creating a LTS tag from the release tag
49178
SHORT_TAG=$(echo "$RELEASE_TAG" | cut -d. -f1,2)
50179
LTS_TAG="${SHORT_TAG}-lts"
51180
echo "LTS_TAG=$LTS_TAG" >> "$GITHUB_OUTPUT"
52181
53-
# Force update the tag to the current commit
54-
git tag -f "$LTS_TAG" $GITHUB_SHA
55-
git push origin -f "$LTS_TAG"
182+
# Get the commit SHA that the release tag points to
183+
RELEASE_SHA=$(git rev-parse "$RELEASE_TAG^{}")
184+
echo "Release tag $RELEASE_TAG points to commit: $RELEASE_SHA"
185+
186+
# Force update the LTS tag to point to the same commit as the release
187+
git tag -f "$LTS_TAG" "$RELEASE_SHA"
188+
git push origin -f "$LTS_TAG"
189+
190+
# Get release body from the original release
191+
RELEASE_BODY=$(gh release view "$RELEASE_TAG" --json body -q .body)
56192
57193
# Write release notes into env (for multiline input)
58194
echo "RELEASE_BODY<<EOF" >> "$GITHUB_ENV"
59195
echo "${RELEASE_BODY}" >> "$GITHUB_ENV"
60196
echo "EOF" >> "$GITHUB_ENV"
61197
62198
- name: Delete existing LTS release (if any)
199+
if: steps.should_proceed.outputs.proceed == 'true'
63200
continue-on-error: true
64201
env:
65202
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -69,18 +206,63 @@ jobs:
69206
gh release delete "$LTS_TAG" -y
70207
71208
- name: Create fresh LTS release
209+
if: steps.should_proceed.outputs.proceed == 'true'
72210
env:
73211
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74212
LTS_TAG: ${{ steps.vars.outputs.LTS_TAG }}
75213
RELEASE_BODY: ${{ env.RELEASE_BODY }}
76214
run: |
77215
echo "Creating new GitHub release for $LTS_TAG"
78216
gh release create "$LTS_TAG" --title "$LTS_TAG" --notes "$RELEASE_BODY"
217+
218+
- name: Set Job Outputs
219+
id: set_outputs
220+
if: steps.should_proceed.outputs.proceed == 'true'
221+
run: |
222+
echo "lts_tag=${{ steps.vars.outputs.LTS_TAG }}" >> "$GITHUB_OUTPUT"
223+
echo "release_tag=${{ steps.set_release_tag.outputs.release_tag }}" >> "$GITHUB_OUTPUT"
224+
echo "Set job outputs for downstream job"
79225
80-
call-publish-image:
81-
name: Publish LTS Image in GHCR
226+
tag-lts-images:
227+
name: Tag Existing Images with LTS
82228
needs: recreate-lts-release
83-
uses: ./.github/workflows/publish.yml
84-
with:
85-
tag: ${{ needs.recreate-lts-release.outputs.lts_tag }}
86-
ref: ${{ github.event.release.tag_name }}
229+
if: |
230+
needs.recreate-lts-release.outputs.lts_tag != '' &&
231+
needs.recreate-lts-release.outputs.release_tag != ''
232+
runs-on: ubuntu-latest
233+
strategy:
234+
matrix:
235+
python-version: ["3.12"]
236+
image-name: ["acapy-agent", "acapy-agent-bbs"]
237+
238+
steps:
239+
- name: Log in to the GitHub Container Registry
240+
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
241+
with:
242+
registry: ghcr.io
243+
username: ${{ github.repository_owner }}
244+
password: ${{ secrets.GITHUB_TOKEN }}
245+
246+
- name: Lowercase Repo Owner
247+
id: lower
248+
run: echo "owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT
249+
250+
- name: Tag Images with LTS
251+
env:
252+
SOURCE_TAG: py${{ matrix.python-version }}-${{ needs.recreate-lts-release.outputs.release_tag }}
253+
LTS_TAG: py${{ matrix.python-version }}-${{ needs.recreate-lts-release.outputs.lts_tag }}
254+
IMAGE_NAME: ghcr.io/${{ steps.lower.outputs.owner }}/${{ matrix.image-name }}
255+
run: |
256+
echo "Tagging \"$IMAGE_NAME:$SOURCE_TAG\" with \"$LTS_TAG\""
257+
258+
# Pull the source image
259+
docker pull "$IMAGE_NAME:$SOURCE_TAG"
260+
261+
# Tag it with the LTS tag
262+
docker tag "$IMAGE_NAME:$SOURCE_TAG" "$IMAGE_NAME:$LTS_TAG"
263+
264+
# Push the new tag
265+
docker push "$IMAGE_NAME:$LTS_TAG"
266+
267+
echo "Successfully tagged \"$IMAGE_NAME\" with \"$LTS_TAG\""
268+

0 commit comments

Comments
 (0)