Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://flexion.us/schemas/openwebui-version-published.json",
"title": "openwebui-version-published",
"description": "Cross-repo dispatch payload sent from flexion/open-webui's publish-flex-image.yml to flexion/flexion-open-webui-infra's open-version-bump-pr.yml after a successful ECR push. The single inter-repo data wire defined in the auto-upgrade pipeline phase 1 design doc.",
"type": "object",
"additionalProperties": false,
"required": ["version", "digest", "environment", "upstream_changelog_url"],
"properties": {
"version": {
"description": "Upstream open-webui release tag, verbatim (keep the leading v).",
"type": "string",
"pattern": "^v\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"
},
"digest": {
"description": "ECR imageDigest captured at push (sha256 hex).",
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"environment": {
"description": "Target environment. Phase 1 only ever fires for dev; the receiver refuses anything else.",
"type": "string",
"enum": ["dev"]
},
"upstream_changelog_url": {
"description": "Operator-readable link to the upstream release page.",
"type": "string",
"format": "uri",
"pattern": "^https://github\\.com/open-webui/open-webui/releases/tag/v\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"
}
}
}
137 changes: 116 additions & 21 deletions .github/workflows/publish-flex-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,175 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version tag for ECR (e.g. v0.9.6). Use the upstream release tag verbatim — keep the 0. prefix.'
description: "Version tag for ECR (e.g. v0.9.6). Use the upstream release tag verbatim — keep the v prefix."
required: true
type: string
environment:
description: 'Target environment'
description: "Target environment"
required: true
type: choice
options:
- dev
- prod
default: dev
skip_build_if_exists:
description: "If the tag already exists in ECR, skip build and just re-fire repository_dispatch."
required: false
type: boolean
default: false
# Push trigger: stage 2 (sync-pr-automerge.yml) squash-merges with an
# `Upstream-Tag: vX.Y.Z` trailer in the commit message. We extract that
# trailer here; if absent, this is a non-sync push (manual flex work,
# backports, etc.) and we exit cleanly.
push:
branches: [flex]

permissions:
id-token: write
contents: read

jobs:
resolve:
name: Resolve trigger and target version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.derive.outputs.version }}
environment: ${{ steps.derive.outputs.environment }}
should_publish: ${{ steps.derive.outputs.should_publish }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 1

- name: Derive target
id: derive
env:
EVENT: ${{ github.event_name }}
DISPATCH_VERSION: ${{ inputs.version }}
DISPATCH_ENV: ${{ inputs.environment }}
run: |
if [ "$EVENT" = "workflow_dispatch" ]; then
echo "version=${DISPATCH_VERSION}" >> "$GITHUB_OUTPUT"
echo "environment=${DISPATCH_ENV}" >> "$GITHUB_OUTPUT"
echo "should_publish=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# push event: extract Upstream-Tag trailer.
TAG=$(git log -1 --pretty=%B | awk '/^Upstream-Tag:/ { print $2; exit }')
if [ -z "$TAG" ]; then
echo "::notice title=Non-sync push::No Upstream-Tag trailer in HEAD commit; skipping publish."
echo "should_publish=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Upstream-Tag value '$TAG' does not match v<major>.<minor>.<patch>"
exit 1
fi
echo "version=${TAG}" >> "$GITHUB_OUTPUT"
echo "environment=dev" >> "$GITHUB_OUTPUT"
echo "should_publish=true" >> "$GITHUB_OUTPUT"

publish:
name: Build flex@${{ github.sha }} → open-webui-${{ inputs.environment }}:${{ inputs.version }}
# ARM-native runner — matches Fargate ARM target, no QEMU emulation needed.
name: Build flex@${{ github.sha }} → open-webui-${{ needs.resolve.outputs.environment }}:${{ needs.resolve.outputs.version }}
needs: resolve
if: needs.resolve.outputs.should_publish == 'true'
runs-on: ubuntu-24.04-arm
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
REPOSITORY: open-webui-${{ inputs.environment }}
REPOSITORY: open-webui-${{ needs.resolve.outputs.environment }}
outputs:
digest: ${{ steps.describe.outputs.digest }}
version: ${{ needs.resolve.outputs.version }}
environment: ${{ needs.resolve.outputs.environment }}
published: ${{ steps.published.outputs.published }}
steps:
- uses: actions/checkout@v6

- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ inputs.environment == 'prod' && secrets.AWS_ROLE_ARN_PROD || secrets.AWS_ROLE_ARN_DEV }}
role-to-assume: ${{ needs.resolve.outputs.environment == 'prod' && secrets.AWS_ROLE_ARN_PROD || secrets.AWS_ROLE_ARN_DEV }}
aws-region: ${{ env.AWS_REGION }}

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

- id: ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Refuse to overwrite an existing tag
- name: Decide build vs skip
id: gate
env:
VERSION: ${{ needs.resolve.outputs.version }}
SKIP_IF_EXISTS: ${{ inputs.skip_build_if_exists }}
run: |
if aws ecr describe-images \
--repository-name "$REPOSITORY" \
--region "$AWS_REGION" \
--image-ids imageTag="${{ inputs.version }}" \
--image-ids imageTag="$VERSION" \
>/dev/null 2>&1; then
echo "::error title=Tag exists::${REPOSITORY}:${{ inputs.version }} already exists in ECR. Promotion is intentionally not idempotent — delete the existing tag manually or pick a different version."
if [ "$SKIP_IF_EXISTS" = "true" ]; then
echo "::notice title=Tag exists::Skipping build, re-firing dispatch only (operator recovery path)."
echo "skip_build=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "::error title=Tag exists::${REPOSITORY}:${VERSION} already exists in ECR. Promotion is intentionally not idempotent — delete the existing tag manually or pick a different version."
exit 1
fi
echo "skip_build=false" >> "$GITHUB_OUTPUT"

- name: Build and push (linux/arm64)
id: publish
if: steps.gate.outputs.skip_build != 'true'
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/arm64
push: true
tags: ${{ steps.ecr.outputs.registry }}/${{ env.REPOSITORY }}:${{ inputs.version }}
tags: ${{ steps.ecr.outputs.registry }}/${{ env.REPOSITORY }}:${{ needs.resolve.outputs.version }}
build-args: |
BUILD_HASH=${{ github.sha }}
USE_PERMISSION_HARDENING=false

- name: Show pushed image
- name: Mark published flag
id: published
run: echo "published=true" >> "$GITHUB_OUTPUT"

- name: Describe pushed image (capture digest)
id: describe
env:
VERSION: ${{ needs.resolve.outputs.version }}
run: |
DIGEST=$(aws ecr describe-images \
--repository-name "$REPOSITORY" \
--region "$AWS_REGION" \
--image-ids imageTag="$VERSION" \
--query 'imageDetails[0].imageDigest' \
--output text)
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
aws ecr describe-images \
--repository-name "$REPOSITORY" \
--region "$AWS_REGION" \
--image-ids imageTag="${{ inputs.version }}" \
--image-ids imageTag="$VERSION" \
--query 'imageDetails[0].{Digest:imageDigest,Tags:imageTags,Pushed:imagePushedAt,SizeBytes:imageSizeInBytes}' \
--output table

- name: Next-step reminder
run: |
cat <<EOF
::notice title=Next step::Image is in ECR but not deployed yet. Open a PR
in flexion/flexion-open-webui-infra changing imageTag in
cdk-infra/lib/cdk-infra-stack.ts to '${{ inputs.version }}' (and update
the matching assertion in cdk-infra/test/cdk-infra.test.ts). Merge to deploy.
See docs/UPGRADING.md in the infra repo.
EOF
dispatch:
name: Dispatch openwebui-version-published to infra repo
needs: [resolve, publish]
# Only fire dispatch for dev — prod promotions stay manual end-to-end.
if: needs.resolve.outputs.environment == 'dev'
runs-on: ubuntu-latest
steps:
- name: repository_dispatch → flexion-open-webui-infra
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.INFRA_REPO_DISPATCH_TOKEN }}
repository: flexion/flexion-open-webui-infra
event-type: openwebui-version-published
# Schema: .github/repository-dispatch/openwebui-version-published.schema.json
client-payload: |
{
"version": "${{ needs.publish.outputs.version }}",
"digest": "${{ needs.publish.outputs.digest }}",
"environment": "${{ needs.publish.outputs.environment }}",
"upstream_changelog_url": "https://github.com/open-webui/open-webui/releases/tag/${{ needs.publish.outputs.version }}"
}
123 changes: 123 additions & 0 deletions .github/workflows/sync-pr-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Stage 2 of the auto-upgrade pipeline.
#
# Auto-merges sync PRs opened by upstream-sync.yml when there are no
# shared-source conflicts (manual_count: 0). On merge, stage 3
# (publish-flex-image.yml) takes over via its push trigger.

name: Sync PR auto-merge

on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]
check_suite:
types: [completed]

permissions:
contents: write
pull-requests: write
checks: read

concurrency:
group: sync-pr-automerge-${{ github.event.pull_request.number || github.event.check_suite.id }}
cancel-in-progress: false

jobs:
evaluate:
if: |
github.event_name == 'check_suite' ||
(github.event_name == 'pull_request_target' && startsWith(github.head_ref, 'upstream-sync/'))
runs-on: ubuntu-latest
steps:
- name: Resolve candidate PR(s)
id: prs
env:
GH_TOKEN: ${{ secrets.SYNC_PAT }}
run: |
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
echo "numbers=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
exit 0
fi
HEAD_SHA="${{ github.event.check_suite.head_sha }}"
NUMS=$(gh pr list --state open --search "in:title chore: upstream-sync" \
--json number,headRefOid,headRefName \
--jq "[.[] | select(.headRefOid == \"$HEAD_SHA\" and (.headRefName | startswith(\"upstream-sync/\"))) | .number] | join(\",\")")
echo "numbers=${NUMS}" >> "$GITHUB_OUTPUT"

- name: Evaluate and merge
if: steps.prs.outputs.numbers != ''
env:
GH_TOKEN: ${{ secrets.SYNC_PAT }}
BOT_LOGIN: flex-upstream-sync-bot
NUMS: ${{ steps.prs.outputs.numbers }}
run: |
set -e
# 15-minute grace window — gives late-arriving check suites time to
# land before we merge. Without this, merging on the first green
# check could race a slow lint job.
GRACE_SECONDS=900

IFS=',' read -ra ARR <<< "$NUMS"
for NUM in "${ARR[@]}"; do
[ -z "$NUM" ] && continue
echo "::group::Evaluating PR #$NUM"
DATA=$(gh pr view "$NUM" --json author,title,body,headRefName,isDraft,mergeable,mergeStateStatus,createdAt)
AUTHOR=$(echo "$DATA" | jq -r '.author.login')
TITLE=$(echo "$DATA" | jq -r '.title')
BODY=$(echo "$DATA" | jq -r '.body')
HEAD=$(echo "$DATA" | jq -r '.headRefName')
DRAFT=$(echo "$DATA" | jq -r '.isDraft')
MERGEABLE=$(echo "$DATA" | jq -r '.mergeable')
STATE=$(echo "$DATA" | jq -r '.mergeStateStatus')
CREATED=$(echo "$DATA" | jq -r '.createdAt')

if [ "$AUTHOR" != "$BOT_LOGIN" ] && [ "$AUTHOR" != "app/${BOT_LOGIN}" ]; then
echo "::notice::author $AUTHOR is not the bot — skipping"
echo "::endgroup::"; continue
fi
case "$HEAD" in
upstream-sync/*) : ;;
*)
echo "::notice::head $HEAD does not match upstream-sync/* — skipping"
echo "::endgroup::"; continue ;;
esac
if [ "$DRAFT" = "true" ]; then
echo "::notice::draft — skipping (manual conflicts present)"
echo "::endgroup::"; continue
fi

# The rebase step writes "Files needing manual review (N)" into
# the PR body. We only auto-merge when N == 0. The body of a
# clean rebase contains "## Clean rebase".
if ! echo "$BODY" | grep -q "## Clean rebase"; then
echo "::notice::PR body does not show a clean rebase — skipping"
echo "::endgroup::"; continue
fi

if [ "$MERGEABLE" != "MERGEABLE" ] || [ "$STATE" != "CLEAN" ]; then
echo "::notice::not yet mergeable (mergeable=$MERGEABLE state=$STATE)"
echo "::endgroup::"; continue
fi

CREATED_TS=$(date -d "$CREATED" +%s)
NOW_TS=$(date -u +%s)
AGE=$((NOW_TS - CREATED_TS))
if [ "$AGE" -lt "$GRACE_SECONDS" ]; then
echo "::notice::PR age ${AGE}s < grace ${GRACE_SECONDS}s — waiting for late checks"
echo "::endgroup::"; continue
fi

# Squash-merge with an Upstream-Tag trailer so stage 3 (publish)
# can extract the target tag from the merged commit message.
TAG=$(echo "$TITLE" | sed -E 's/.*onto (v[0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Could not extract upstream tag from title: $TITLE"
echo "::endgroup::"; continue
fi
COMMIT_MSG=$(printf 'chore: upstream-sync flex onto %s\n\nUpstream-Tag: %s\n' "$TAG" "$TAG")

echo "PR #$NUM eligible — squash-merging with Upstream-Tag: $TAG"
gh pr merge "$NUM" --squash --delete-branch \
--subject "chore: upstream-sync flex onto ${TAG} (#${NUM})" \
--body "Upstream-Tag: ${TAG}"
echo "::endgroup::"
done
Loading