Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a91334f
ci: switch Build & Release to git tag driven flow
neilkuan Apr 9, 2026
c7ee28d
ci: mark beta chart releases as pre-release
neilkuan Apr 9, 2026
aebdc3b
ci: split chart release — beta pushes OCI only, stable opens PR
neilkuan Apr 9, 2026
160a729
ci: add resolve-tag job, fix workflow_dispatch bug, deduplicate
neilkuan Apr 9, 2026
2d58aff
ci: stable release promotes beta image instead of rebuilding
neilkuan Apr 9, 2026
74e6308
ci: add tagpr workflow and config for automated release PR
neilkuan Apr 9, 2026
3c628f3
ci: pin tagpr action to commit hash and bump checkout to v6
neilkuan Apr 9, 2026
4ec6a63
ci: add tagpr workflow and config for automated release PR
neilkuan Apr 9, 2026
c11bcfe
ci: add promote-stable to guarantee pre-release = stable image
neilkuan Apr 9, 2026
5ba3242
docs: rewrite RELEASING.md with complete release flow and constraints
neilkuan Apr 9, 2026
84c1902
ci: promote-stable finds pre-release image by version tag, not commit…
neilkuan Apr 9, 2026
aa09333
chore: update openab version to 0.6.0 in Cargo.lock
neilkuan Apr 9, 2026
e3169f8
ci: upgrade tagpr to v1.18.1 and use vPrefix config
neilkuan Apr 11, 2026
ce48efa
ci: replace deprecated app-id with client-id in create-github-app-token
neilkuan Apr 11, 2026
2a1bc5d
ci: add CI workflow for PR validation on source and Dockerfile changes
neilkuan Apr 11, 2026
e79d71a
docs: add GitHub App permissions to RELEASING.md
neilkuan Apr 11, 2026
7fb9cde
ci: replace tagpr with native GitHub workflows
thepagent Apr 11, 2026
fef071a
ci: address review feedback
thepagent Apr 11, 2026
f7c98da
fix: replace map_or with is_some_and to satisfy clippy
thepagent Apr 11, 2026
530c520
chore: use beta instead of rc for pre-release naming
thepagent Apr 11, 2026
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
239 changes: 135 additions & 104 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,17 @@ name: Build & Release

on:
push:
branches:
- main
paths:
- "src/**"
- "Cargo.toml"
- "Cargo.lock"
- "Dockerfile"
- "Dockerfile.*"
tags:
- "v*"
workflow_dispatch:
inputs:
chart_bump:
description: 'Chart version bump type'
tag:
description: 'Version tag (e.g. v0.7.0-beta.1 or v0.7.0)'
required: true
type: choice
options:
- patch
- minor
- major
default: patch
release:
description: 'Stable release (no beta suffix)'
required: false
type: boolean
default: false
type: string
default: 'v'
dry_run:
description: 'Dry run (show changes without committing)'
description: 'Dry run (build only, no push)'
required: false
type: boolean
default: false
Expand All @@ -37,7 +22,46 @@ env:
IMAGE_NAME: ${{ github.repository }}

jobs:
resolve-tag:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.resolve.outputs.tag }}
chart_version: ${{ steps.resolve.outputs.chart_version }}
is_prerelease: ${{ steps.resolve.outputs.is_prerelease }}
steps:
- name: Resolve and validate tag
id: resolve
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
else
TAG="${GITHUB_REF_NAME}"
fi

# Validate tag format
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "::error::Invalid tag format '${TAG}'. Expected v{major}.{minor}.{patch}[-prerelease]"
exit 1
fi

CHART_VERSION="${TAG#v}"

# Pre-release if version contains '-' (e.g. 0.7.0-beta.1)
if [[ "$CHART_VERSION" == *-* ]]; then
IS_PRERELEASE="true"
else
IS_PRERELEASE="false"
fi

echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "chart_version=${CHART_VERSION}" >> "$GITHUB_OUTPUT"
echo "is_prerelease=${IS_PRERELEASE}" >> "$GITHUB_OUTPUT"

# ── Pre-release path: full build ──────────────────────────────

build-image:
needs: resolve-tag
if: ${{ needs.resolve-tag.outputs.is_prerelease == 'true' }}
strategy:
matrix:
variant:
Expand All @@ -53,19 +77,19 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
- uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}

Expand Down Expand Up @@ -96,8 +120,8 @@ jobs:
retention-days: 1

merge-manifests:
needs: build-image
if: inputs.dry_run != true
needs: [resolve-tag, build-image]
if: ${{ inputs.dry_run != true && needs.resolve-tag.outputs.is_prerelease == 'true' }}
strategy:
matrix:
variant:
Expand All @@ -109,8 +133,6 @@ jobs:
permissions:
contents: read
packages: write
outputs:
version: ${{ steps.meta.outputs.version }}
steps:
- name: Download digests
uses: actions/download-artifact@v4
Expand All @@ -121,109 +143,118 @@ jobs:

- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
- uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}
tags: |
type=sha,prefix=
type=raw,value=latest
type=semver,pattern={{version}},value=${{ needs.resolve-tag.outputs.tag }}

- name: Create manifest list
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}@sha256:%s ' *)

bump-chart:
needs: merge-manifests
if: inputs.dry_run != true
# ── Stable path: promote pre-release image (no rebuild) ──────

promote-stable:
needs: resolve-tag
if: ${{ inputs.dry_run != true && needs.resolve-tag.outputs.is_prerelease == 'false' }}
strategy:
matrix:
variant:
- { suffix: "" }
- { suffix: "-codex" }
- { suffix: "-claude" }
- { suffix: "-gemini" }
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
contents: read
packages: write
steps:
- name: Generate App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Get current chart version
id: current
run: |
chart_version=$(grep '^version:' charts/openab/Chart.yaml | awk '{print $2}')
echo "chart_version=$chart_version" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3

- name: Bump chart version
id: bump
- uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Find pre-release image
id: find-prerelease
run: |
current="${{ steps.current.outputs.chart_version }}"
# Strip any existing pre-release suffix for base version
base="${current%%-*}"
IFS='.' read -r major minor patch <<< "$base"
bump_type="${{ inputs.chart_bump }}"
bump_type="${bump_type:-patch}"
case "$bump_type" in
major) major=$((major + 1)); minor=0; patch=0 ;;
minor) minor=$((minor + 1)); patch=0 ;;
patch) patch=$((patch + 1)) ;;
esac
# Stable release: clean version. Otherwise: beta with run number.
if [ "${{ inputs.release }}" = "true" ]; then
new_version="${major}.${minor}.${patch}"
else
new_version="${major}.${minor}.${patch}-beta.${GITHUB_RUN_NUMBER}"
CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
# Find latest pre-release tag matching this version (e.g. v0.7.0-beta.1)
PRERELEASE_TAG=$(git tag -l "v${CHART_VERSION}-*" --sort=-v:refname | head -1)
if [ -z "$PRERELEASE_TAG" ]; then
echo "::error::No pre-release tag found for v${CHART_VERSION}-*. Run a pre-release build first."
exit 1
fi
echo "new_version=$new_version" >> "$GITHUB_OUTPUT"
PRERELEASE_VERSION="${PRERELEASE_TAG#v}"
echo "Found pre-release: ${PRERELEASE_TAG} (${PRERELEASE_VERSION})"
echo "prerelease_version=${PRERELEASE_VERSION}" >> "$GITHUB_OUTPUT"

- name: Resolve image SHA
id: image-sha
- name: Verify pre-release image exists
run: |
# Use the commit SHA that triggered this build — this is the SHA
# that merge-manifests tagged the Docker image with (type=sha,prefix=).
# We capture it here explicitly so it survives the bump commit.
IMAGE_SHA="${{ github.sha }}"
IMAGE_SHA="${IMAGE_SHA:0:7}"
echo "sha=${IMAGE_SHA}" >> "$GITHUB_OUTPUT"

- name: Update Chart.yaml and values.yaml
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}"
PRERELEASE_VERSION="${{ steps.find-prerelease.outputs.prerelease_version }}"
echo "Checking ${IMAGE}:${PRERELEASE_VERSION} ..."
docker buildx imagetools inspect "${IMAGE}:${PRERELEASE_VERSION}" || \
{ echo "::error::Image ${IMAGE}:${PRERELEASE_VERSION} not found — build the pre-release first"; exit 1; }

- name: Promote to stable tags
run: |
IMAGE_SHA="${{ steps.image-sha.outputs.sha }}"
sed -i "s/^version: .*/version: ${{ steps.bump.outputs.new_version }}/" charts/openab/Chart.yaml
sed -i "s/^appVersion: .*/appVersion: \"${IMAGE_SHA}\"/" charts/openab/Chart.yaml
sed -i "s|repository: .*|repository: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}|" charts/openab/values.yaml
sed -i "s/tag: .*/tag: \"${IMAGE_SHA}\"/" charts/openab/values.yaml

- name: Create and auto-merge bump PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}${{ matrix.variant.suffix }}"
PRERELEASE_VERSION="${{ steps.find-prerelease.outputs.prerelease_version }}"
CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
MAJOR_MINOR="${CHART_VERSION%.*}"

echo "Promoting ${IMAGE}:${PRERELEASE_VERSION} → ${CHART_VERSION}, ${MAJOR_MINOR}, latest"
docker buildx imagetools create \
-t "${IMAGE}:${CHART_VERSION}" \
-t "${IMAGE}:${MAJOR_MINOR}" \
-t "${IMAGE}:latest" \
"${IMAGE}:${PRERELEASE_VERSION}"

# ── Chart release (runs after either path) ───────────────────

release-chart:
needs: [resolve-tag, merge-manifests, promote-stable]
if: >-
${{ always() && inputs.dry_run != true &&
needs.resolve-tag.result == 'success' &&
(needs.merge-manifests.result == 'success' || needs.promote-stable.result == 'success') }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6

- name: Install Helm
uses: azure/setup-helm@v4

- uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push chart to OCI
run: |
VERSION="${{ steps.bump.outputs.new_version }}"
IMAGE_SHA="${{ steps.image-sha.outputs.sha }}"
BRANCH="chore/chart-${VERSION}"
git config user.name "openab-app[bot]"
git config user.email "274185012+openab-app[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"
git add charts/openab/Chart.yaml charts/openab/values.yaml
git commit -m "chore: bump chart to ${VERSION}

image: ${IMAGE_SHA}"
git push origin "$BRANCH"
PR_URL=$(gh pr create \
--title "chore: bump chart to ${VERSION}" \
--body "Auto-generated chart version bump for image \`${IMAGE_SHA}\`." \
--base main --head "$BRANCH")
gh pr merge "$PR_URL" --squash --delete-branch
CHART_VERSION="${{ needs.resolve-tag.outputs.chart_version }}"
helm package charts/openab
helm push openab-${CHART_VERSION}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
pull_request:
paths:
- "src/**"
- "Cargo.toml"
- "Cargo.lock"
- "Dockerfile*"

env:
CARGO_TERM_COLOR: always

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- uses: Swatinem/rust-cache@v2

- name: cargo check
run: cargo check

- name: cargo clippy
run: cargo clippy -- -D warnings

- name: cargo test
run: cargo test
Loading