Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .config/mise/lib/calver.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# shellcheck shell=bash
# decide <digest> <today YYYY.MDD> reads "<tag> <digest>" lines of the
# existing version tags on stdin. It prints "released <tag>" when a
# tag already points at <digest>, otherwise "next <YYYY.MDD.N>".
decide() {
local digest="$1" today="$2" tag tagdigest n=0
while read -r tag tagdigest; do
[ -n "$tag" ] || continue
if [ "$tagdigest" = "$digest" ]; then
echo "released $tag"
return 0
fi
case "$tag" in "$today".*) n=$((n + 1)) ;; esac
done
echo "next $today.$n"
}
18 changes: 18 additions & 0 deletions .config/mise/lib/ghcr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# shellcheck shell=bash
# ghcr_manifest_status <image> <tag> <actor> <token> prints the HTTP status of
# the manifest. The registry's anonymous token endpoint is not enough for a
# private package, so the bearer token is requested with the given credentials.
ghcr_manifest_status() {
local image="$1" tag="$2" actor="$3" token="$4" bearer
# Command substitution does not inherit errexit, so fail the token request
# here instead of sending an empty bearer and reporting a misleading 401.
bearer="$(curl -fsS -u "$actor:$token" \
"https://ghcr.io/token?service=ghcr.io&scope=repository:${image#ghcr.io/}:pull" | jq -er .token)" || {
echo "ghcr.io token request failed for $image" >&2
return 1
}
curl -sS -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $bearer" \
-H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"https://ghcr.io/v2/${image#ghcr.io/}/manifests/$tag"
}
5 changes: 5 additions & 0 deletions .config/mise/lib/image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# shellcheck shell=bash
# shellcheck disable=SC2034
# <registry>/images/<namespace>/<name>: the namespace is the source repository
# and the name is the deployable (ADR 6497710059).
IMAGE=ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Expose the image name as the step output name"
set -euo pipefail
source "$MISE_PROJECT_ROOT/.config/mise/lib/image.sh"
echo "name=$IMAGE" >> "$GITHUB_OUTPUT"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
#MISE description="Refresh the model catalogs with upstream's script"
set -euo pipefail
exec bash "$MISE_PROJECT_ROOT/.github/scripts/refresh-model-catalogs.sh"
4 changes: 4 additions & 0 deletions .config/mise/tasks/github/workflows/trogonstack-image/summary
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
#MISE description="Write the pushed image reference to the step summary"
set -euo pipefail
echo "\`${REF}\`" >> "$GITHUB_STEP_SUMMARY"
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
#MISE description="Add the next CalVer tag to the image built for the commit"
set -euo pipefail
source "$MISE_PROJECT_ROOT/.config/mise/lib/image.sh"
source "$MISE_PROJECT_ROOT/.config/mise/lib/calver.sh"

digest="$(crane digest "$IMAGE:$COMMIT" 2>/dev/null)" || {
echo "No image $IMAGE:$COMMIT. Run trogonstack-image for that commit first." >&2
exit 1
}

# A registry error must stop the release: a missing pair would hide an earlier
# release of this commit or reuse a taken N. Only "no version tags" is fine.
all_tags="$(crane ls "$IMAGE")"
pairs=""
for tag in $(printf '%s\n' "$all_tags" | grep -E '^[0-9]{4}\.[0-9]{3,4}\.[0-9]+$' || true); do
tagdigest="$(crane digest "$IMAGE:$tag")"
pairs+="$tag $tagdigest"$'\n'
done

# Month without padding followed by a two digit day: 5 April is 405.
today="$(date -u +%Y.%-m%d)"
result="$(printf '%s\n' "$pairs" | decide "$digest" "$today")"

case "$result" in
released\ *)
tag="${result#released }"
echo "$COMMIT already released as $tag ($digest)"
printf 'Already released as %s\n\n%s@%s\n' "$tag" "$IMAGE" "$digest" >> "$GITHUB_STEP_SUMMARY"
exit 0
;;
esac

version="${result#next }"
crane tag "$IMAGE:$COMMIT" "$version"

tagged="$(crane digest "$IMAGE:$version")"
if [ "$tagged" != "$digest" ]; then
echo "Tag $version resolves to $tagged, expected $digest" >&2
exit 1
fi
printf 'Released %s\n\n%s:%s@%s\n' "$version" "$IMAGE" "$version" "$digest" >> "$GITHUB_STEP_SUMMARY"
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
#MISE description="Log crane in to ghcr.io"
set -euo pipefail
printf '%s' "$TOKEN" | crane auth login ghcr.io -u "$ACTOR" --password-stdin
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Resolve the commit to release"
set -euo pipefail
sha="$(gh api "repos/$GITHUB_REPOSITORY/commits/${INPUT_COMMIT:-main}" --jq .sha)"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
#MISE description="Merge the upstream tag, or open a conflict issue and fail"
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
if git merge --no-ff --no-edit -m "chore(sync): merge upstream $TAG" "refs/tags/$TAG"; then
echo "conflict=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "conflict=true" >> "$GITHUB_OUTPUT"
git diff --name-only --diff-filter=U > "$RUNNER_TEMP/conflicts.txt"
git merge --abort
title="Upstream sync conflict: $TAG"
{
echo "Merging upstream $TAG into main hit conflicts."
echo
echo "Conflicting files:"
sed 's/^/- /' "$RUNNER_TEMP/conflicts.txt"
echo
echo "Run: $RUN_URL"
echo
echo "Resolve locally with: git merge $TAG, then push the result to main."
} > "$RUNNER_TEMP/conflict-body.txt"
# gh under GITHUB_TOKEN is non-interactive: pass the body by file so no
# editor or prompt is needed, and skip labels that may not exist.
existing="$(gh issue list --state open --limit 200 --json number,title \
--jq "[.[] | select(.title == \"$title\")] | length")"
if [ "$existing" = "0" ]; then
gh issue create --title "$title" --body-file "$RUNNER_TEMP/conflict-body.txt"
else
echo "Open conflict issue already exists"
fi
exit 1
16 changes: 16 additions & 0 deletions .config/mise/tasks/github/workflows/trogonstack-sync/pick-tag
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
#MISE description="Pick the newest stable upstream tag and report whether main contains it"
set -euo pipefail
git fetch --no-tags "$UPSTREAM" '+refs/tags/v*:refs/tags/v*'
tag="$(git tag -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)"
if [ -z "$tag" ]; then
echo "No stable upstream tag found" >&2
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
if git merge-base --is-ancestor "refs/tags/$tag" HEAD; then
echo "up_to_date=true" >> "$GITHUB_OUTPUT"
echo "main already contains $tag" >> "$GITHUB_STEP_SUMMARY"
else
echo "up_to_date=false" >> "$GITHUB_OUTPUT"
fi
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
#MISE description="Publish a merge that fails verification as a pull request and fail"
set -euo pipefail
branch="sync/$TAG"
git fetch origin "$branch" || true
git push --force-with-lease origin "HEAD:refs/heads/$branch"
title="chore(sync): merge upstream $TAG"
{
echo "Merging upstream $TAG into main is clean but build or tests failed."
echo
echo "Failed run: $RUN_URL"
echo
echo "Fix on this branch, then merge it into main."
} > "$RUNNER_TEMP/pr-body.txt"
existing="$(gh pr list --state open --head "$branch" --json number --jq 'length')"
if [ "$existing" = "0" ]; then
gh pr create --base main --head "$branch" --title "$title" \
--body-file "$RUNNER_TEMP/pr-body.txt"
else
echo "Open sync PR already exists"
fi
exit 1
26 changes: 26 additions & 0 deletions .config/mise/tasks/github/workflows/trogonstack-sync/push-main
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
#MISE description="Push the verified merge to main and dispatch the image build"
set -euo pipefail
# GITHUB_TOKEN cannot push commits that touch .github/workflows, and
# upstream changes those every month or so. Say so in an issue
# instead of leaving a bare red run.
if ! git push origin HEAD:main; then
title="Upstream sync needs a manual push: $TAG"
if git diff --quiet HEAD^1 HEAD -- .github/workflows; then
reason="The push to main was rejected."
else
reason="The merge changes .github/workflows, which GITHUB_TOKEN may not push."
fi
printf '%s\n\nRun: %s\n\nPush it locally with: git merge %s, then git push origin main.\n' \
"$reason" "$RUN_URL" "$TAG" > "$RUNNER_TEMP/push-body.txt"
existing="$(gh issue list --state open --limit 200 --json title \
--jq "[.[] | select(.title == \"$title\")] | length")"
if [ "$existing" = "0" ]; then
gh issue create --title "$title" --body-file "$RUNNER_TEMP/push-body.txt"
fi
exit 1
fi
# Pushes made with GITHUB_TOKEN do not trigger workflows, but
# workflow_dispatch does, so start the image build explicitly.
gh workflow run trogonstack-image.yml --ref main -f release=true
echo "Merged upstream $TAG into main and dispatched the image build with release" >> "$GITHUB_STEP_SUMMARY"
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
#MISE description="Release current main, building it first when it has no image"
set -euo pipefail
source "$MISE_PROJECT_ROOT/.config/mise/lib/image.sh"
source "$MISE_PROJECT_ROOT/.config/mise/lib/ghcr.sh"

head="$(git rev-parse HEAD)"
# shellcheck disable=SC2153
status="$(ghcr_manifest_status "$IMAGE" "$head" "$ACTOR" "$REGISTRY_TOKEN")"
# Only a 404 means the image is missing. Any other answer is an outage
# or an auth problem, and building again would move the commit tag.
if [ "$status" = "200" ]; then
# Release the commit whose image was checked, even if main moves meanwhile.
gh workflow run trogonstack-release.yml --ref main -f commit="$head"
echo "main is current with upstream; dispatched the release for $head" >> "$GITHUB_STEP_SUMMARY"
elif [ "$status" = "404" ]; then
gh workflow run trogonstack-image.yml --ref main -f release=true
echo "main is current with upstream; $IMAGE:$head not found, dispatched the image build with release" >> "$GITHUB_STEP_SUMMARY"
else
echo "Registry answered HTTP $status for $IMAGE:$head" >&2
exit 1
fi
5 changes: 5 additions & 0 deletions .config/mise/tasks/github/workflows/trogonstack-sync/verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Build and test the merged tree"
set -euo pipefail
go build ./...
go test ./...
24 changes: 24 additions & 0 deletions .github/actions/setup-mise/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Set up mise
description: Install a pinned mise release after verifying its sha256 checksum.

# jdx/mise-action is not an allowed action, so the release binary is installed
# directly. This runs before mise exists, so it cannot be a mise task
# (ADR 6647967335 puts composite action scripts under
# .config/mise/tasks/github/actions/<name>/ only when mise can run them).
runs:
using: composite
steps:
- shell: bash
env:
MISE_VERSION: v2026.10.15
MISE_SHA256: 1c2d85dc42bef51e0bb20780093c746d6e12dfd15ca461f3979bd52cc214bc19
run: |
set -euo pipefail
archive="$RUNNER_TEMP/mise.tar.gz"
curl -fsSL -o "$archive" "https://github.com/jdx/mise/releases/download/$MISE_VERSION/mise-$MISE_VERSION-linux-x64.tar.gz"
echo "$MISE_SHA256 $archive" | sha256sum --check --strict
mkdir -p "$RUNNER_TEMP/mise-install"
tar -xzf "$archive" -C "$RUNNER_TEMP/mise-install"
echo "$RUNNER_TEMP/mise-install/mise/bin" >> "$GITHUB_PATH"
# The checkout is ours, so its mise.toml needs no trust prompt.
echo "MISE_TRUSTED_CONFIG_PATHS=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
70 changes: 43 additions & 27 deletions .github/workflows/trogonstack-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,22 @@ name: trogonstack-image

on:
push:
branches: [main, 'yordis/**']
branches: [main]
pull_request:
workflow_dispatch:
inputs:
release:
description: Tag the built image with the next CalVer version (main only)
type: boolean
default: false

permissions:
contents: read

env:
IMAGE: ghcr.io/trogonstack/cli-proxy-api
# A pull request pushes only when it comes from this repository, so fork PRs
# build without a registry write.
PUSH: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}

jobs:
image:
Expand All @@ -27,49 +34,58 @@ jobs:
with:
go-version-file: go.mod
cache: true
- uses: ./.github/actions/setup-mise
- id: image
run: mise run github:workflows:trogonstack-image:image-name
- name: Refresh models catalog
run: bash .github/scripts/refresh-model-catalogs.sh
run: mise run github:workflows:trogonstack-image:refresh-model-catalogs
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
if: github.event_name != 'pull_request'
if: env.PUSH == 'true'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute fork version tag
id: forkver
if: github.ref == 'refs/heads/main'
run: |
# Upstream tags are fetched into the runner only, never pushed back.
git fetch --no-tags https://github.com/router-for-me/CLIProxyAPI.git '+refs/tags/v*:refs/tags/v*'
base="$(git tag --merged HEAD -l 'v*' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n 1)"
if [ -z "$base" ]; then
echo "No upstream release tag reachable from HEAD" >&2
exit 1
fi
n="$(git rev-list --count "$base..HEAD")"
echo "value=${base}-trogonstack.${n}" >> "$GITHUB_OUTPUT"
echo "Fork image tag: \`${base}-trogonstack.${n}\`" >> "$GITHUB_STEP_SUMMARY"
- id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
env:
# Annotations land on both the image index and its manifest.
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
# Label the pull request head commit, not the merge commit.
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: ${{ env.IMAGE }}
images: ${{ steps.image.outputs.name }}
tags: |
type=sha,format=long
type=ref,event=branch
type=raw,value=${{ steps.forkver.outputs.value || 'unused' }},enable=${{ github.ref == 'refs/heads/main' }}
type=sha,format=long,prefix=,enable=${{ github.event_name != 'pull_request' }}
type=raw,value=pr-${{ github.event.pull_request.head.sha }},enable=${{ github.event_name == 'pull_request' }}
- id: build
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
with:
context: .
# The ADR requires a multi-platform image index. The cluster is amd64
# only, so the index holds one platform plus its provenance
# attestation. It exists so arm64 can be added here without changing
# any tag or path.
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
provenance: mode=min
push: ${{ env.PUSH == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
annotations: ${{ steps.meta.outputs.annotations }}
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
- if: github.event_name != 'pull_request'
COMMIT=${{ github.event.pull_request.head.sha || github.sha }}
- if: env.PUSH == 'true'
env:
REF: ${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
run: echo "\`${REF}\`" >> "$GITHUB_STEP_SUMMARY"
REF: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }}
run: mise run github:workflows:trogonstack-image:summary

release:
needs: image
if: github.event_name == 'workflow_dispatch' && inputs.release && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
uses: ./.github/workflows/trogonstack-release.yml
with:
commit: ${{ github.sha }}
Loading
Loading