Skip to content

Commit ed16c65

Browse files
committed
implement release workflow
1 parent 6a158cb commit ed16c65

8 files changed

Lines changed: 357 additions & 41 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Build and push release candidate
2+
3+
on:
4+
push:
5+
branches:
6+
- 'release/*'
7+
8+
jobs:
9+
tests:
10+
uses: ./.github/workflows/reusable-tests.yml
11+
secrets:
12+
codecov_token: ${{ secrets.CODECOV_TOKEN }}
13+
14+
build_docker:
15+
needs: tests
16+
uses: ./.github/workflows/reusable-docker.yml
17+
18+
validate:
19+
needs: build_docker
20+
permissions:
21+
contents: write
22+
uses: ./.github/workflows/reusable-gate.yml
23+
with:
24+
tag: true
25+
26+
push_docker:
27+
needs: validate
28+
permissions:
29+
packages: write
30+
uses: ./.github/workflows/reusable-docker.yml
31+
with:
32+
push: true
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Build and push release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
tests:
10+
uses: ./.github/workflows/reusable-tests.yml
11+
secrets:
12+
codecov_token: ${{ secrets.CODECOV_TOKEN }}
13+
14+
build_docker:
15+
needs: tests
16+
uses: ./.github/workflows/reusable-docker.yml
17+
18+
validate:
19+
needs: build_docker
20+
permissions:
21+
contents: write
22+
uses: ./.github/workflows/reusable-gate.yml
23+
with:
24+
tag: true
25+
26+
push_docker:
27+
needs: validate
28+
permissions:
29+
packages: write
30+
uses: ./.github/workflows/reusable-docker.yml
31+
with:
32+
push: true
33+
34+
backmerge_main_to_develop:
35+
needs: push_docker
36+
runs-on: ubuntu-latest
37+
permissions:
38+
pull-requests: write
39+
steps:
40+
- uses: actions/checkout@v4
41+
with:
42+
fetch-depth: 0
43+
44+
- name: Create PR from main to develop
45+
env:
46+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
run: |
48+
current_tag=$(git tag --points-at HEAD --sort=creatordate | tail -n1)
49+
50+
gh pr create \
51+
--base develop \
52+
--head main \
53+
--title "Backmerge main into develop after release ${current_tag}" \
54+
--body "Automated backmerge PR from \`main\` into \`develop\` after release ${current_tag}."
55+
56+
create_new_release:
57+
needs: push_docker
58+
runs-on: ubuntu-latest
59+
permissions:
60+
contents: write
61+
steps:
62+
- uses: actions/checkout@v4
63+
with:
64+
fetch-depth: 0
65+
66+
- name: Create GitHub Release
67+
env:
68+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69+
run: |
70+
current_tag=$(git tag --points-at HEAD --sort=creatordate | tail -n1)
71+
72+
gh release create "${current_tag}" \
73+
--title "Release ${current_tag}" \
74+
--generate-notes \
75+
--draft
76+
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,23 @@
11
# Builds the batchee Docker image on PRs and pushes.
2-
# Publishes to GHCR only on version tag pushes.
3-
name: Build batchee image
2+
name: Reusable docker workflow
43

54
on:
6-
pull_request:
7-
branches: [develop, main, 'release/**']
8-
push:
9-
tags:
10-
- '[0-9]+.[0-9]+.[0-9]+'
11-
- '[0-9]+.[0-9]+.[0-9]+rc[0-9]+'
12-
13-
concurrency:
14-
group: ${{ github.workflow }}-${{ github.ref }}
15-
cancel-in-progress: true
5+
workflow_call:
6+
inputs:
7+
push:
8+
type: boolean
9+
required: false
10+
default: false
1611

1712
env:
1813
PYTHON_VERSION: "3.12"
1914
REGISTRY: ghcr.io
2015
IMAGE_NAME: ${{ github.repository }}
2116

2217
jobs:
23-
tests:
24-
uses: ./.github/workflows/run_tests.yml
25-
secrets:
26-
codecov_token: ${{ secrets.CODECOV_TOKEN }}
27-
28-
build:
29-
needs: tests
18+
docker:
3019
runs-on: ubuntu-latest
31-
permissions:
32-
contents: read
33-
packages: write
20+
3421
steps:
3522
- name: Retrieve repository
3623
uses: actions/checkout@v6
@@ -46,14 +33,13 @@ jobs:
4633
id: version
4734
run: |
4835
SERVICE_VERSION="$(uv tool run hatch version)"
49-
echo "service_version=$SERVICE_VERSION" >> "$GITHUB_OUTPUT"
50-
51-
- name: Log in to the Container registry
52-
uses: docker/login-action@v4
53-
with:
54-
registry: ${{ env.REGISTRY }}
55-
username: ${{ github.actor }}
56-
password: ${{ secrets.GITHUB_TOKEN }}
36+
if [[ "${SERVICE_VERSION}" =~ rc[0-9]+$ ]]; then
37+
target_env=uat
38+
else
39+
target_env=ops
40+
fi
41+
echo "service_version=${SERVICE_VERSION}" >> "$GITHUB_OUTPUT"
42+
echo "target_env=$target_env" >> "$GITHUB_OUTPUT"
5743
5844
- name: Extract metadata (tags, labels) for Docker
5945
id: meta
@@ -62,9 +48,16 @@ jobs:
6248
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
6349
tags: |
6450
type=raw,value=${{ steps.version.outputs.service_version }}
65-
type=raw,value=uat,enable=${{ startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'rc') }}
66-
type=raw,value=ops,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, 'rc') }}
67-
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, 'rc') }}
51+
type=raw,value=${{ steps.version.outputs.target_env }}
52+
type=raw,value=latest,enable=${{ steps.version.outputs.target_env == 'ops' }}
53+
54+
- name: Log in to the Container registry
55+
if: ${{ inputs.push }}
56+
uses: docker/login-action@v4
57+
with:
58+
registry: ${{ env.REGISTRY }}
59+
username: ${{ github.actor }}
60+
password: ${{ secrets.GITHUB_TOKEN }}
6861

6962
- name: Set up Docker Buildx
7063
uses: docker/setup-buildx-action@v4
@@ -76,9 +69,12 @@ jobs:
7669
file: Dockerfile
7770
build-args: |
7871
SERVICE_VERSION=${{ steps.version.outputs.service_version }}
79-
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
72+
push: ${{ inputs.push }}
8073
pull: true
74+
platforms: linux/amd64
75+
provenance: false
76+
sbom: false
8177
tags: ${{ steps.meta.outputs.tags }}
8278
labels: ${{ steps.meta.outputs.labels }}
83-
cache-from: type=gha
84-
cache-to: type=gha,mode=max
79+
cache-from: type=gha,scope=${{ (github.head_ref || github.ref_name) }}
80+
cache-to: type=gha,mode=max,scope=${{ (github.head_ref || github.ref_name) }}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
name: Reusable gate
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
tag:
7+
required: false
8+
type: boolean
9+
default: false
10+
11+
env:
12+
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
13+
14+
jobs:
15+
validate_release:
16+
if: ${{ (github.head_ref || github.ref_name) == 'main' }}
17+
runs-on: ubuntu-latest
18+
outputs:
19+
next_tag: ${{ steps.validate.outputs.next_tag }}
20+
21+
steps:
22+
- name: Retrieve repository
23+
uses: actions/checkout@v6
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Validate last RC tag
28+
id: validate
29+
run: |
30+
# validate last rc tag
31+
last_rc=$(git describe --tags --match "*rc*" --abbrev=0)
32+
33+
if [[ -z "${last_rc}" ]]; then
34+
echo "No RC tags found"
35+
exit 1
36+
fi
37+
38+
if [[ "${last_rc}" =~ ^([0-9]+\.[0-9]+\.[0-9]+)rc([0-9]+)$ ]]; then
39+
rc_base="${BASH_REMATCH[1]}"
40+
else
41+
echo "Could not parse RC tag: ${last_rc}"
42+
exit 2
43+
fi
44+
45+
if [[ -n "$(git tag --list "${rc_base}")" ]]; then
46+
echo "Stable tag ${rc_base} already exists."
47+
exit 3
48+
fi
49+
50+
echo "Last RC tag: ${last_rc}"
51+
echo "RC base version: ${rc_base}"
52+
53+
echo "next_tag=${rc_base}" >> "$GITHUB_OUTPUT"
54+
55+
validate_candidate:
56+
if: ${{ startsWith(github.head_ref || github.ref_name, 'release/') }}
57+
runs-on: ubuntu-latest
58+
outputs:
59+
next_tag: ${{ steps.validate.outputs.next_tag }}
60+
61+
steps:
62+
- name: Retrieve repository
63+
uses: actions/checkout@v6
64+
with:
65+
fetch-depth: 0
66+
67+
- name: Validate branch name
68+
id: validate
69+
run: |
70+
# strictly validating the format of branch name
71+
if [[ ! "${BRANCH_NAME}" =~ ^release/(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
72+
echo "Invalid branch '${BRANCH_NAME}'. Expected 'release/X.Y.Z' (e.g. release/1.2.3)"
73+
exit 1
74+
fi
75+
76+
new_major="${BASH_REMATCH[1]}"
77+
new_minor="${BASH_REMATCH[2]}"
78+
new_patch="${BASH_REMATCH[3]}"
79+
new_version="${new_major}.${new_minor}.${new_patch}"
80+
81+
highest_tag=$(git tag --list | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)
82+
83+
if [[ -z "${highest_tag}" ]]; then
84+
echo "No valid semantic version tags found"
85+
exit 2
86+
fi
87+
88+
IFS='.' read -r old_major old_minor old_patch <<< "${highest_tag}"
89+
90+
allowed_patch="${old_major}.${old_minor}.$((old_patch + 1))"
91+
allowed_minor="${old_major}.$((old_minor + 1)).0"
92+
allowed_major="$((old_major + 1)).0.0"
93+
94+
if [[ "${new_version}" != "${allowed_patch}" && \
95+
"${new_version}" != "${allowed_minor}" && \
96+
"${new_version}" != "${allowed_major}" ]]; then
97+
echo "Release version '${new_version}' is not exactly one valid increment from highest existing stable tag '${highest_tag}'."
98+
echo "Allowed next versions are: ${allowed_patch}, ${allowed_minor}, ${allowed_major}."
99+
exit 3
100+
fi
101+
102+
echo "Highest existing stable tag: ${highest_tag}"
103+
echo "Validated new release version: ${new_version}"
104+
105+
last_rc=$(git tag --list "${new_version}rc*" | sort -V | tail -n1)
106+
if [[ -z "${last_rc}" ]]; then
107+
next_tag="${new_version}rc1"
108+
elif [[ "${last_rc}" =~ ^${new_version}rc([0-9]+)$ ]]; then
109+
rc="${BASH_REMATCH[1]}"
110+
next_tag="${new_version}rc$((rc+1))"
111+
else
112+
echo "Found matching tag candidate but could not parse RC number: ${last_rc}"
113+
exit 4
114+
fi
115+
116+
echo "Next RC tag: ${next_tag}"
117+
echo "next_tag=${next_tag}" >> "$GITHUB_OUTPUT"
118+
119+
push_tag:
120+
needs: [validate_release, validate_candidate]
121+
if: |
122+
always() &&
123+
inputs.tag &&
124+
(
125+
needs.validate_release.outputs.next_tag != '' ||
126+
needs.validate_candidate.outputs.next_tag != ''
127+
)
128+
129+
runs-on: ubuntu-latest
130+
131+
env:
132+
NEXT_TAG: >-
133+
${{ needs.validate_release.outputs.next_tag ||
134+
needs.validate_candidate.outputs.next_tag }}
135+
136+
steps:
137+
- name: Retrieve repository
138+
uses: actions/checkout@v6
139+
with:
140+
fetch-depth: 0
141+
142+
- name: Push tag
143+
run: |
144+
# pushing the tag
145+
git config user.name "github-actions"
146+
git config user.email "github-actions@github.com"
147+
148+
git tag -a "${NEXT_TAG}" -m "tag ${NEXT_TAG}"
149+
git push origin "${NEXT_TAG}"
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55

66
# Runs linting and unit tests on push, and reports
77
# code coverage to Codecov.
8-
name: Run tests and linting
8+
name: Reusable tests and linting workflow
99

1010
on:
11-
push:
1211
workflow_call:
1312
secrets:
1413
codecov_token:
@@ -22,9 +21,6 @@ jobs:
2221
run_tests:
2322
runs-on: ubuntu-latest
2423

25-
permissions:
26-
contents: read
27-
2824
steps:
2925
- name: Retrieve repository
3026
uses: actions/checkout@v6

0 commit comments

Comments
 (0)