mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
* chore(release): bump version to v3.8.5 * fix(docker): rebuild better-sqlite3 after hardened install (#2772) Integrated into release/v3.8.5 * ci: build Docker platforms on native runners (#2774) Integrated into release/v3.8.5 * docs(release): sync v3.8.5 documentation and metadata Update changelog entries, API reference version, package metadata, and localized LLM documentation for the 3.8.5 release. Refresh generated docs source mappings and architecture counts for current executors and OAuth providers. * chore(release): bump to v3.8.5 — changelog, docs, version sync * chore(release): translate Hall of Contributors to English in workflows and changelog * fix(combos): make target timeout configurable (#2775) Merge PR #2775 — fix(combos): make target timeout configurable * feat: fix so restart of server restarts batch jobs instead of failing them (#2755) Merge PR #2755 — feat: fix so restart of server restarts batch jobs instead of failing them * chore(release): update changelog with merged PRs notes and credits * feat(api): add endpoint restrictions for client API keys (#2777) Merge PR #2777 — feat(api): add endpoint restrictions for client API keys * chore(release): update changelog with PR #2777 entry and contributor credit --------- Co-authored-by: Thanet S. <cho.112543@gmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Markus Hartung <mail@hartmark.se> Co-authored-by: Jack <5443152+hijak@users.noreply.github.com>
296 lines
9.7 KiB
YAML
296 lines
9.7 KiB
YAML
name: Publish to Docker Hub
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
tags:
|
|
- "v*"
|
|
paths-ignore:
|
|
- ".github/workflows/**"
|
|
# Use 'released' instead of 'published' so editing/re-publishing old releases
|
|
# does NOT re-trigger this workflow. 'released' fires only on the initial
|
|
# release publication (and pre-release → release transition).
|
|
release:
|
|
types: [released]
|
|
workflow_dispatch:
|
|
inputs:
|
|
version:
|
|
description: "Version tag to build (e.g. 3.8.4)"
|
|
required: true
|
|
type: string
|
|
promote_latest:
|
|
description: "Also tag :latest (only if this is the highest semver)"
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
jobs:
|
|
prepare:
|
|
name: Resolve Docker release metadata
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
version: ${{ steps.version.outputs.version }}
|
|
promote_latest: ${{ steps.version.outputs.promote_latest }}
|
|
skip: ${{ steps.version.outputs.skip }}
|
|
env:
|
|
IMAGE_NAME: diegosouzapw/omniroute
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
|
# Need full tag history for semver comparison when deciding :latest.
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve version, latest-promotion, and skip flag
|
|
id: version
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
REF_NAME: ${{ github.ref_name }}
|
|
REF_TYPE: ${{ github.ref_type }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
PROMOTE_INPUT: ${{ inputs.promote_latest }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# 1) Resolve version string from the trigger (all inputs come via env).
|
|
case "$EVENT_NAME" in
|
|
workflow_dispatch)
|
|
VERSION="${INPUT_VERSION#v}"
|
|
;;
|
|
push)
|
|
if [ "$REF_TYPE" = "tag" ]; then
|
|
VERSION="${REF_NAME#v}"
|
|
else
|
|
# Push to main → build & tag as `main` only. Never touch :latest.
|
|
VERSION="main"
|
|
fi
|
|
;;
|
|
release)
|
|
VERSION="${REF_NAME#v}"
|
|
;;
|
|
*)
|
|
VERSION="${REF_NAME#v}"
|
|
;;
|
|
esac
|
|
# Sanity-check: only allow [A-Za-z0-9._-] in VERSION (defense in depth).
|
|
if ! printf '%s' "$VERSION" | grep -qE '^[A-Za-z0-9._-]+$'; then
|
|
echo "Refusing to use unsafe VERSION value: $VERSION" >&2
|
|
exit 1
|
|
fi
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
|
|
# 2) Decide whether to promote :latest.
|
|
PROMOTE="false"
|
|
if [ "$VERSION" = "main" ]; then
|
|
PROMOTE="false"
|
|
elif printf '%s' "$VERSION" | grep -qE -- '-(rc|alpha|beta|pre|next)'; then
|
|
echo "Pre-release identifier detected — skipping :latest."
|
|
PROMOTE="false"
|
|
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
|
PROMOTE="${PROMOTE_INPUT:-false}"
|
|
else
|
|
git fetch --tags --quiet || true
|
|
HIGHEST=$(git tag -l 'v[0-9]*' | sed 's/^v//' | grep -vE -- '-(rc|alpha|beta|pre|next)' | sort -V | tail -1 || echo "")
|
|
if [ -n "$HIGHEST" ] && [ "$VERSION" = "$HIGHEST" ]; then
|
|
PROMOTE="true"
|
|
else
|
|
echo "Version $VERSION is not the highest semver tag (highest=${HIGHEST:-<none>}). Not promoting :latest."
|
|
fi
|
|
fi
|
|
echo "promote_latest=$PROMOTE" >> "$GITHUB_OUTPUT"
|
|
|
|
# 3) Skip if this exact version is already published in Docker Hub.
|
|
# `main` is always rebuilt (mutable floating tag).
|
|
SKIP="false"
|
|
if [ "$VERSION" != "main" ]; then
|
|
if docker manifest inspect "diegosouzapw/omniroute:${VERSION}" >/dev/null 2>&1; then
|
|
echo "Image diegosouzapw/omniroute:${VERSION} already exists on Docker Hub — skipping rebuild."
|
|
SKIP="true"
|
|
fi
|
|
fi
|
|
echo "skip=$SKIP" >> "$GITHUB_OUTPUT"
|
|
|
|
echo "Publishing diegosouzapw/omniroute:$VERSION (promote_latest=$PROMOTE, skip=$SKIP)"
|
|
|
|
build:
|
|
name: Build Docker (${{ matrix.platform }})
|
|
needs: prepare
|
|
if: needs.prepare.outputs.skip != 'true'
|
|
runs-on: ${{ matrix.runner }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- platform: linux/amd64
|
|
runner: ubuntu-24.04
|
|
arch: amd64
|
|
- platform: linux/arm64
|
|
runner: ubuntu-24.04-arm
|
|
arch: arm64
|
|
env:
|
|
IMAGE_NAME: diegosouzapw/omniroute
|
|
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Login to Docker Hub
|
|
uses: docker/login-action@v4
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push platform image by digest
|
|
id: build
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
target: runner-base
|
|
platforms: ${{ matrix.platform }}
|
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
|
tags: |
|
|
${{ env.IMAGE_NAME }}
|
|
${{ env.GHCR_IMAGE_NAME }}
|
|
cache-from: type=gha,scope=docker-${{ matrix.arch }}
|
|
cache-to: type=gha,scope=docker-${{ matrix.arch }},mode=max
|
|
no-cache: false
|
|
env:
|
|
DOCKER_BUILDKIT_INLINE_CACHE: 1
|
|
|
|
- name: Export digest
|
|
env:
|
|
DIGEST: ${{ steps.build.outputs.digest }}
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p /tmp/digests
|
|
digest="${DIGEST#sha256:}"
|
|
touch "/tmp/digests/${digest}"
|
|
|
|
- name: Upload digest
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: digests-${{ matrix.arch }}
|
|
path: /tmp/digests/*
|
|
if-no-files-found: error
|
|
retention-days: 1
|
|
|
|
merge:
|
|
name: Publish multi-arch manifests
|
|
needs:
|
|
- prepare
|
|
- build
|
|
if: needs.prepare.outputs.skip != 'true'
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
IMAGE_NAME: diegosouzapw/omniroute
|
|
GHCR_IMAGE_NAME: ghcr.io/diegosouzapw/omniroute
|
|
VERSION: ${{ needs.prepare.outputs.version }}
|
|
PROMOTE_LATEST: ${{ needs.prepare.outputs.promote_latest }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
|
fetch-depth: 0
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Login to Docker Hub
|
|
uses: docker/login-action@v4
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Download digests
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: digests-*
|
|
path: /tmp/digests
|
|
merge-multiple: true
|
|
|
|
- name: Create Docker Hub manifest
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
tags=(-t "${IMAGE_NAME}:${VERSION}")
|
|
if [ "$PROMOTE_LATEST" = "true" ]; then
|
|
tags+=(-t "${IMAGE_NAME}:latest")
|
|
fi
|
|
|
|
refs=()
|
|
while IFS= read -r digest_file; do
|
|
refs+=("${IMAGE_NAME}@sha256:$(basename "$digest_file")")
|
|
done < <(find /tmp/digests -type f | sort)
|
|
|
|
if [ "${#refs[@]}" -eq 0 ]; then
|
|
echo "No image digests were downloaded." >&2
|
|
exit 1
|
|
fi
|
|
|
|
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
|
|
|
- name: Create GHCR manifest
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
tags=(-t "${GHCR_IMAGE_NAME}:${VERSION}")
|
|
if [ "$PROMOTE_LATEST" = "true" ]; then
|
|
tags+=(-t "${GHCR_IMAGE_NAME}:latest")
|
|
fi
|
|
|
|
refs=()
|
|
while IFS= read -r digest_file; do
|
|
refs+=("${GHCR_IMAGE_NAME}@sha256:$(basename "$digest_file")")
|
|
done < <(find /tmp/digests -type f | sort)
|
|
|
|
if [ "${#refs[@]}" -eq 0 ]; then
|
|
echo "No image digests were downloaded." >&2
|
|
exit 1
|
|
fi
|
|
|
|
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
|
|
|
|
- name: Inspect image
|
|
if: needs.prepare.outputs.version != 'main'
|
|
run: |
|
|
docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}"
|
|
|
|
- name: Update Docker Hub description
|
|
# Only refresh README/description when we actually promote :latest
|
|
# (avoids overwriting from main pushes or back-fill builds).
|
|
if: needs.prepare.outputs.promote_latest == 'true'
|
|
uses: peter-evans/dockerhub-description@v5
|
|
with:
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
repository: diegosouzapw/omniroute
|
|
short-description: "OmniRoute — Unified AI proxy. Route any LLM through one endpoint."
|
|
readme-filepath: ./README.md
|