Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/build-release-candidate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Build and push release candidate

on:
push:
branches:
- 'release/*'

concurrency:
group: release-publish
cancel-in-progress: false

jobs:
tests:
uses: ./.github/workflows/reusable-tests.yml
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}

build_docker:
needs: tests
uses: ./.github/workflows/reusable-docker.yml

validate:
needs: build_docker
permissions:
contents: write
uses: ./.github/workflows/reusable-gate.yml
with:
tag: true

push_docker:
needs: validate
permissions:
packages: write
uses: ./.github/workflows/reusable-docker.yml
with:
push: true
79 changes: 79 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Build and push release

on:
push:
branches:
- main

concurrency:
group: release-publish
cancel-in-progress: false

jobs:
tests:
uses: ./.github/workflows/reusable-tests.yml
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}

build_docker:
needs: tests
uses: ./.github/workflows/reusable-docker.yml

validate:
needs: build_docker
permissions:
contents: write
uses: ./.github/workflows/reusable-gate.yml
with:
tag: true

push_docker:
needs: validate
permissions:
packages: write
uses: ./.github/workflows/reusable-docker.yml
with:
push: true

backmerge_main_to_develop:
needs: push_docker
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Create PR from main to develop
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
current_tag=$(git tag --points-at HEAD --sort=creatordate | tail -n1)

gh pr create \
--base develop \
--head main \
--title "Backmerge main into develop after release ${current_tag}" \
--body "Automated backmerge PR from \`main\` into \`develop\` after release ${current_tag}."

create_new_release:
needs: push_docker
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
current_tag=$(git tag --points-at HEAD --sort=creatordate | tail -n1)

gh release create "${current_tag}" \
--title "Release ${current_tag}" \
--generate-notes \
--draft
Original file line number Diff line number Diff line change
@@ -1,36 +1,23 @@
# Builds the batchee Docker image on PRs and pushes.
# Publishes to GHCR only on version tag pushes.
name: Build batchee image
name: Reusable docker workflow

on:
pull_request:
branches: [develop, main, 'release/**']
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+'
- '[0-9]+.[0-9]+.[0-9]+rc[0-9]+'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
workflow_call:
inputs:
push:
type: boolean
required: false
default: false

env:
PYTHON_VERSION: "3.12"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
tests:
uses: ./.github/workflows/run_tests.yml
secrets:
codecov_token: ${{ secrets.CODECOV_TOKEN }}

build:
needs: tests
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Retrieve repository
uses: actions/checkout@v6
Expand All @@ -46,14 +33,13 @@ jobs:
id: version
run: |
SERVICE_VERSION="$(uv tool run hatch version)"
echo "service_version=$SERVICE_VERSION" >> "$GITHUB_OUTPUT"

- name: Log in to the Container registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
if [[ "${SERVICE_VERSION}" =~ rc[0-9]+$ ]]; then
target_env=uat
else
target_env=ops
fi
echo "service_version=${SERVICE_VERSION}" >> "$GITHUB_OUTPUT"
echo "target_env=$target_env" >> "$GITHUB_OUTPUT"

- name: Extract metadata (tags, labels) for Docker
id: meta
Expand All @@ -62,9 +48,16 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ steps.version.outputs.service_version }}
type=raw,value=uat,enable=${{ startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'rc') }}
type=raw,value=ops,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, 'rc') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') && !contains(github.ref_name, 'rc') }}
type=raw,value=${{ steps.version.outputs.target_env }}
type=raw,value=latest,enable=${{ steps.version.outputs.target_env == 'ops' }}

- name: Log in to the Container registry
if: ${{ inputs.push }}
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Expand All @@ -76,9 +69,12 @@ jobs:
file: Dockerfile
build-args: |
SERVICE_VERSION=${{ steps.version.outputs.service_version }}
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
push: ${{ inputs.push }}
pull: true
platforms: linux/amd64
provenance: false
sbom: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=${{ (github.head_ref || github.ref_name) }}
cache-to: type=gha,mode=max,scope=${{ (github.head_ref || github.ref_name) }}
149 changes: 149 additions & 0 deletions .github/workflows/reusable-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: Reusable gate

on:
workflow_call:
inputs:
tag:
required: false
type: boolean
default: false

env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}

jobs:
validate_release:
if: ${{ (github.head_ref || github.ref_name) == 'main' }}
runs-on: ubuntu-latest
outputs:
next_tag: ${{ steps.validate.outputs.next_tag }}

steps:
- name: Retrieve repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Validate last RC tag
id: validate
run: |
# validate last rc tag
last_rc=$(git describe --tags --match "*rc*" --abbrev=0)

if [[ -z "${last_rc}" ]]; then
echo "No RC tags found"
exit 1
fi

if [[ "${last_rc}" =~ ^([0-9]+\.[0-9]+\.[0-9]+)rc([0-9]+)$ ]]; then
rc_base="${BASH_REMATCH[1]}"
else
echo "Could not parse RC tag: ${last_rc}"
exit 2
fi

if [[ -n "$(git tag --list "${rc_base}")" ]]; then
echo "Stable tag ${rc_base} already exists."
exit 3
fi

echo "Last RC tag: ${last_rc}"
echo "RC base version: ${rc_base}"

echo "next_tag=${rc_base}" >> "$GITHUB_OUTPUT"

validate_candidate:
if: ${{ startsWith(github.head_ref || github.ref_name, 'release/') }}
runs-on: ubuntu-latest
outputs:
next_tag: ${{ steps.validate.outputs.next_tag }}

steps:
- name: Retrieve repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Validate branch name
id: validate
run: |
# strictly validating the format of branch name
if [[ ! "${BRANCH_NAME}" =~ ^release/(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "Invalid branch '${BRANCH_NAME}'. Expected 'release/X.Y.Z' (e.g. release/1.2.3)"
exit 1
fi

new_major="${BASH_REMATCH[1]}"
new_minor="${BASH_REMATCH[2]}"
new_patch="${BASH_REMATCH[3]}"
new_version="${new_major}.${new_minor}.${new_patch}"

highest_tag=$(git tag --list | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)

if [[ -z "${highest_tag}" ]]; then
echo "No valid semantic version tags found"
exit 2
fi

IFS='.' read -r old_major old_minor old_patch <<< "${highest_tag}"

allowed_patch="${old_major}.${old_minor}.$((old_patch + 1))"
allowed_minor="${old_major}.$((old_minor + 1)).0"
allowed_major="$((old_major + 1)).0.0"

if [[ "${new_version}" != "${allowed_patch}" && \
"${new_version}" != "${allowed_minor}" && \
"${new_version}" != "${allowed_major}" ]]; then
echo "Release version '${new_version}' is not exactly one valid increment from highest existing stable tag '${highest_tag}'."
echo "Allowed next versions are: ${allowed_patch}, ${allowed_minor}, ${allowed_major}."
exit 3
fi

echo "Highest existing stable tag: ${highest_tag}"
echo "Validated new release version: ${new_version}"

last_rc=$(git tag --list "${new_version}rc*" | sort -V | tail -n1)
if [[ -z "${last_rc}" ]]; then
next_tag="${new_version}rc1"
elif [[ "${last_rc}" =~ ^${new_version}rc([0-9]+)$ ]]; then
rc="${BASH_REMATCH[1]}"
next_tag="${new_version}rc$((rc+1))"
else
echo "Found matching tag candidate but could not parse RC number: ${last_rc}"
exit 4
fi

echo "Next RC tag: ${next_tag}"
echo "next_tag=${next_tag}" >> "$GITHUB_OUTPUT"

push_tag:
needs: [validate_release, validate_candidate]
if: |
always() &&
inputs.tag &&
(
needs.validate_release.outputs.next_tag != '' ||
needs.validate_candidate.outputs.next_tag != ''
)

runs-on: ubuntu-latest

env:
NEXT_TAG: >-
${{ needs.validate_release.outputs.next_tag ||
needs.validate_candidate.outputs.next_tag }}

steps:
- name: Retrieve repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Push tag
run: |
# pushing the tag
git config user.name "github-actions"
git config user.email "github-actions@github.com"

git tag -a "${NEXT_TAG}" -m "tag ${NEXT_TAG}"
git push origin "${NEXT_TAG}"
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@

# Runs linting and unit tests on push, and reports
# code coverage to Codecov.
name: Run tests and linting
name: Reusable tests and linting workflow

on:
push:
workflow_call:
secrets:
codecov_token:
Expand All @@ -22,9 +21,6 @@ jobs:
run_tests:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Retrieve repository
uses: actions/checkout@v6
Expand Down
Loading