From 08421da03aed5aa679f676c0412327eb8e315b5d Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Nov 2026 16:39:42 -0500 Subject: [PATCH 1/4] feat(ci): publish the image under the OCI artifact path and tags ADR The image now lives at images/cliproxyapi/cli-proxy-api and is tagged only with the full commit SHA, or pr- for same-repo pull requests, so a digest can always be traced to git and no tag ever moves. Annotations on both the index and the manifest carry source and revision, and the image is pushed as an index so arm64 can be added later without changing tags or paths. Signed-off-by: Yordis Prieto --- .github/workflows/trogonstack-image.yml | 62 ++++++----- .github/workflows/trogonstack-release.yml | 120 ++++++++++++++++++++++ .github/workflows/trogonstack-sync.yml | 37 ++++++- 3 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/trogonstack-release.yml diff --git a/.github/workflows/trogonstack-image.yml b/.github/workflows/trogonstack-image.yml index 9c1823ce898..047ae253806 100644 --- a/.github/workflows/trogonstack-image.yml +++ b/.github/workflows/trogonstack-image.yml @@ -2,15 +2,25 @@ 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 + # /images//: the namespace is the source repository + # and the name is the deployable (ADR 6497710059). + # 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 }} + IMAGE: ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api jobs: image: @@ -31,45 +41,51 @@ jobs: run: bash .github/scripts/refresh-model-catalogs.sh - 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 }} 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" + + 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 }} diff --git a/.github/workflows/trogonstack-release.yml b/.github/workflows/trogonstack-release.yml new file mode 100644 index 00000000000..376e0c2267b --- /dev/null +++ b/.github/workflows/trogonstack-release.yml @@ -0,0 +1,120 @@ +name: trogonstack-release + +# Releases an image by adding a CalVer tag (YYYY.MDD.N, ADR 6497710059) to the +# image already built for a commit. It never builds, so the digest of the +# release is the digest of the commit build. Safe to run repeatedly: a commit +# that already carries a version tag is left alone. + +on: + workflow_call: + inputs: + commit: + description: Full commit SHA to release. Defaults to the current HEAD of main. + type: string + required: false + default: '' + workflow_dispatch: + inputs: + commit: + description: Commit to release. Defaults to the current HEAD of main. + type: string + required: false + default: '' + +permissions: + contents: read + +concurrency: + group: trogonstack-release + cancel-in-progress: false + +env: + IMAGE: ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api + CRANE: github.com/google/go-containerregistry/cmd/crane@v0.22.1 + GH_TOKEN: ${{ github.token }} + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + packages: write + steps: + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version: '1.25' + cache: false + - name: Install crane + run: | + go install "$CRANE" + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + - name: Log in to ghcr.io + env: + ACTOR: ${{ github.actor }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: printf '%s' "$TOKEN" | crane auth login ghcr.io -u "$ACTOR" --password-stdin + - name: Resolve the commit + id: commit + env: + INPUT_COMMIT: ${{ inputs.commit }} + run: | + sha="$(gh api "repos/$GITHUB_REPOSITORY/commits/${INPUT_COMMIT:-main}" --jq .sha)" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + - name: Add the CalVer tag + env: + COMMIT: ${{ steps.commit.outputs.sha }} + run: | + set -euo pipefail + + # BEGIN decide + # decide reads " " lines of the + # existing version tags on stdin. It prints "released " when a + # tag already points at , otherwise "next ". + 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" + } + # END decide + + digest="$(crane digest "$IMAGE:$COMMIT" 2>/dev/null)" || { + echo "No image $IMAGE:$COMMIT. Run trogonstack-image for that commit first." >&2 + exit 1 + } + + pairs="$( + crane ls "$IMAGE" | grep -E '^[0-9]{4}\.[0-9]{3,4}\.[0-9]+$' | while read -r tag; do + echo "$tag $(crane digest "$IMAGE:$tag")" + done + )" || true + + # 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" diff --git a/.github/workflows/trogonstack-sync.yml b/.github/workflows/trogonstack-sync.yml index 084273b01ca..0bff593fb84 100644 --- a/.github/workflows/trogonstack-sync.yml +++ b/.github/workflows/trogonstack-sync.yml @@ -16,6 +16,7 @@ permissions: pull-requests: write issues: write actions: write + packages: read concurrency: group: trogonstack-sync @@ -25,6 +26,7 @@ env: UPSTREAM: https://github.com/router-for-me/CLIProxyAPI.git GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} + IMAGE: ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} jobs: @@ -159,5 +161,36 @@ jobs: 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 - echo "Merged upstream $TAG into main and dispatched the image build" >> "$GITHUB_STEP_SUMMARY" + 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" + + # Main may hold fork-only commits that were never released. The release + # workflow is idempotent, so run it for HEAD when it has an image, and + # build and release that commit when it has none. + - name: Release current main + if: steps.tag.outputs.up_to_date == 'true' + env: + REGISTRY_TOKEN: ${{ github.token }} + ACTOR: ${{ github.actor }} + run: | + head="$(git rev-parse HEAD)" + # The registry's anonymous token endpoint is not enough for a private + # package, so ask with the workflow token. + bearer="$(curl -fsS -u "$ACTOR:$REGISTRY_TOKEN" \ + "https://ghcr.io/token?service=ghcr.io&scope=repository:${IMAGE#ghcr.io/}:pull" | jq -r .token)" + status="$(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/$head")" + # 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 + gh workflow run trogonstack-release.yml --ref main + 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 From 671e32fd8e6a9b24b186805c5a7f69c4f32c726d Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Nov 2026 16:43:27 -0500 Subject: [PATCH 2/4] refactor(ci): run trogonstack workflow steps as mise tasks ADR 6647967335 requires every script a workflow runs to be a mise file task and shared code to live under .config/mise/lib. Workflow steps now only call mise run, and the image name, ghcr manifest check and CalVer decide function are sourced from one place each. mise is installed from a pinned release with a sha256 check because jdx/mise-action is not an allowed action. That step runs before mise exists, so it stays inline in the composite action. crane is now a mise tool, which removes the Go toolchain from the release workflow. Upstream's refresh script stays where it is to avoid merge conflicts; a task calls it. Signed-off-by: Yordis Prieto --- .config/mise/lib/calver.sh | 16 +++ .config/mise/lib/ghcr.sh | 13 ++ .config/mise/lib/image.sh | 5 + .../workflows/trogonstack-image/image-name | 5 + .../trogonstack-image/refresh-model-catalogs | 4 + .../workflows/trogonstack-image/summary | 4 + .../trogonstack-release/add-calver-tag | 39 ++++++ .../trogonstack-release/registry-login | 4 + .../trogonstack-release/resolve-commit | 5 + .../workflows/trogonstack-sync/merge-upstream | 33 +++++ .../workflows/trogonstack-sync/pick-tag | 16 +++ .../trogonstack-sync/publish-pull-request | 22 +++ .../workflows/trogonstack-sync/push-main | 26 ++++ .../trogonstack-sync/release-current | 21 +++ .../github/workflows/trogonstack-sync/verify | 5 + .github/actions/setup-mise/action.yml | 22 +++ .github/workflows/trogonstack-image.yml | 14 +- .github/workflows/trogonstack-release.yml | 74 +--------- .github/workflows/trogonstack-sync.yml | 128 ++---------------- mise.toml | 4 + 20 files changed, 266 insertions(+), 194 deletions(-) create mode 100644 .config/mise/lib/calver.sh create mode 100644 .config/mise/lib/ghcr.sh create mode 100644 .config/mise/lib/image.sh create mode 100755 .config/mise/tasks/github/workflows/trogonstack-image/image-name create mode 100755 .config/mise/tasks/github/workflows/trogonstack-image/refresh-model-catalogs create mode 100755 .config/mise/tasks/github/workflows/trogonstack-image/summary create mode 100755 .config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag create mode 100755 .config/mise/tasks/github/workflows/trogonstack-release/registry-login create mode 100755 .config/mise/tasks/github/workflows/trogonstack-release/resolve-commit create mode 100755 .config/mise/tasks/github/workflows/trogonstack-sync/merge-upstream create mode 100755 .config/mise/tasks/github/workflows/trogonstack-sync/pick-tag create mode 100755 .config/mise/tasks/github/workflows/trogonstack-sync/publish-pull-request create mode 100755 .config/mise/tasks/github/workflows/trogonstack-sync/push-main create mode 100755 .config/mise/tasks/github/workflows/trogonstack-sync/release-current create mode 100755 .config/mise/tasks/github/workflows/trogonstack-sync/verify create mode 100644 .github/actions/setup-mise/action.yml create mode 100644 mise.toml diff --git a/.config/mise/lib/calver.sh b/.config/mise/lib/calver.sh new file mode 100644 index 00000000000..1d4aeecad9e --- /dev/null +++ b/.config/mise/lib/calver.sh @@ -0,0 +1,16 @@ +# shellcheck shell=bash +# decide reads " " lines of the +# existing version tags on stdin. It prints "released " when a +# tag already points at , otherwise "next ". +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" +} diff --git a/.config/mise/lib/ghcr.sh b/.config/mise/lib/ghcr.sh new file mode 100644 index 00000000000..5d3fd191561 --- /dev/null +++ b/.config/mise/lib/ghcr.sh @@ -0,0 +1,13 @@ +# shellcheck shell=bash +# ghcr_manifest_status 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 + bearer="$(curl -fsS -u "$actor:$token" \ + "https://ghcr.io/token?service=ghcr.io&scope=repository:${image#ghcr.io/}:pull" | jq -r .token)" + 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" +} diff --git a/.config/mise/lib/image.sh b/.config/mise/lib/image.sh new file mode 100644 index 00000000000..28ce9e46adc --- /dev/null +++ b/.config/mise/lib/image.sh @@ -0,0 +1,5 @@ +# shellcheck shell=bash +# shellcheck disable=SC2034 +# /images//: the namespace is the source repository +# and the name is the deployable (ADR 6497710059). +IMAGE=ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api diff --git a/.config/mise/tasks/github/workflows/trogonstack-image/image-name b/.config/mise/tasks/github/workflows/trogonstack-image/image-name new file mode 100755 index 00000000000..460554fb683 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-image/image-name @@ -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" diff --git a/.config/mise/tasks/github/workflows/trogonstack-image/refresh-model-catalogs b/.config/mise/tasks/github/workflows/trogonstack-image/refresh-model-catalogs new file mode 100755 index 00000000000..b686e7f350b --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-image/refresh-model-catalogs @@ -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" diff --git a/.config/mise/tasks/github/workflows/trogonstack-image/summary b/.config/mise/tasks/github/workflows/trogonstack-image/summary new file mode 100755 index 00000000000..591265f83ce --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-image/summary @@ -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" diff --git a/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag b/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag new file mode 100755 index 00000000000..64ff7c87f6f --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag @@ -0,0 +1,39 @@ +#!/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 +} + +pairs="$( + crane ls "$IMAGE" | grep -E '^[0-9]{4}\.[0-9]{3,4}\.[0-9]+$' | while read -r tag; do + echo "$tag $(crane digest "$IMAGE:$tag")" + done +)" || true + +# 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" diff --git a/.config/mise/tasks/github/workflows/trogonstack-release/registry-login b/.config/mise/tasks/github/workflows/trogonstack-release/registry-login new file mode 100755 index 00000000000..176e1cfd949 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-release/registry-login @@ -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 diff --git a/.config/mise/tasks/github/workflows/trogonstack-release/resolve-commit b/.config/mise/tasks/github/workflows/trogonstack-release/resolve-commit new file mode 100755 index 00000000000..c578d58f1d2 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-release/resolve-commit @@ -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" diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/merge-upstream b/.config/mise/tasks/github/workflows/trogonstack-sync/merge-upstream new file mode 100755 index 00000000000..e0526daf8b9 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/merge-upstream @@ -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 diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/pick-tag b/.config/mise/tasks/github/workflows/trogonstack-sync/pick-tag new file mode 100755 index 00000000000..7067d11a8f8 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/pick-tag @@ -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 diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/publish-pull-request b/.config/mise/tasks/github/workflows/trogonstack-sync/publish-pull-request new file mode 100755 index 00000000000..03369b7ae69 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/publish-pull-request @@ -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 diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/push-main b/.config/mise/tasks/github/workflows/trogonstack-sync/push-main new file mode 100755 index 00000000000..09f9f7b5b7f --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/push-main @@ -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" diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/release-current b/.config/mise/tasks/github/workflows/trogonstack-sync/release-current new file mode 100755 index 00000000000..366c05b74d5 --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/release-current @@ -0,0 +1,21 @@ +#!/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 + gh workflow run trogonstack-release.yml --ref main + 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 diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/verify b/.config/mise/tasks/github/workflows/trogonstack-sync/verify new file mode 100755 index 00000000000..d718a97323d --- /dev/null +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/verify @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +#MISE description="Build and test the merged tree" +set -euo pipefail +go build ./... +go test ./... diff --git a/.github/actions/setup-mise/action.yml b/.github/actions/setup-mise/action.yml new file mode 100644 index 00000000000..733932d2533 --- /dev/null +++ b/.github/actions/setup-mise/action.yml @@ -0,0 +1,22 @@ +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// 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" diff --git a/.github/workflows/trogonstack-image.yml b/.github/workflows/trogonstack-image.yml index 047ae253806..53c0e7903c6 100644 --- a/.github/workflows/trogonstack-image.yml +++ b/.github/workflows/trogonstack-image.yml @@ -15,12 +15,9 @@ permissions: contents: read env: - # /images//: the namespace is the source repository - # and the name is the deployable (ADR 6497710059). # 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 }} - IMAGE: ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api jobs: image: @@ -37,8 +34,11 @@ 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: env.PUSH == 'true' @@ -54,7 +54,7 @@ jobs: # 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,prefix=,enable=${{ github.event_name != 'pull_request' }} type=raw,value=pr-${{ github.event.pull_request.head.sha }},enable=${{ github.event_name == 'pull_request' }} @@ -77,8 +77,8 @@ jobs: 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 diff --git a/.github/workflows/trogonstack-release.yml b/.github/workflows/trogonstack-release.yml index 376e0c2267b..492ef671480 100644 --- a/.github/workflows/trogonstack-release.yml +++ b/.github/workflows/trogonstack-release.yml @@ -29,8 +29,6 @@ concurrency: cancel-in-progress: false env: - IMAGE: ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api - CRANE: github.com/google/go-containerregistry/cmd/crane@v0.22.1 GH_TOKEN: ${{ github.token }} jobs: @@ -41,80 +39,22 @@ jobs: contents: read packages: write steps: - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + # The mise tasks and configuration live in the repository. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - go-version: '1.25' - cache: false - - name: Install crane - run: | - go install "$CRANE" - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + persist-credentials: false + - uses: ./.github/actions/setup-mise - name: Log in to ghcr.io env: ACTOR: ${{ github.actor }} TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: printf '%s' "$TOKEN" | crane auth login ghcr.io -u "$ACTOR" --password-stdin + run: mise run github:workflows:trogonstack-release:registry-login - name: Resolve the commit id: commit env: INPUT_COMMIT: ${{ inputs.commit }} - run: | - sha="$(gh api "repos/$GITHUB_REPOSITORY/commits/${INPUT_COMMIT:-main}" --jq .sha)" - echo "sha=$sha" >> "$GITHUB_OUTPUT" + run: mise run github:workflows:trogonstack-release:resolve-commit - name: Add the CalVer tag env: COMMIT: ${{ steps.commit.outputs.sha }} - run: | - set -euo pipefail - - # BEGIN decide - # decide reads " " lines of the - # existing version tags on stdin. It prints "released " when a - # tag already points at , otherwise "next ". - 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" - } - # END decide - - digest="$(crane digest "$IMAGE:$COMMIT" 2>/dev/null)" || { - echo "No image $IMAGE:$COMMIT. Run trogonstack-image for that commit first." >&2 - exit 1 - } - - pairs="$( - crane ls "$IMAGE" | grep -E '^[0-9]{4}\.[0-9]{3,4}\.[0-9]+$' | while read -r tag; do - echo "$tag $(crane digest "$IMAGE:$tag")" - done - )" || true - - # 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" + run: mise run github:workflows:trogonstack-release:add-calver-tag diff --git a/.github/workflows/trogonstack-sync.yml b/.github/workflows/trogonstack-sync.yml index 0bff593fb84..ac774d13147 100644 --- a/.github/workflows/trogonstack-sync.yml +++ b/.github/workflows/trogonstack-sync.yml @@ -26,7 +26,6 @@ env: UPSTREAM: https://github.com/router-for-me/CLIProxyAPI.git GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} - IMAGE: ghcr.io/trogonstack/images/cliproxyapi/cli-proxy-api RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} jobs: @@ -39,60 +38,17 @@ jobs: ref: main fetch-depth: 0 + - uses: ./.github/actions/setup-mise + - name: Pick newest stable upstream tag id: tag - run: | - 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 - + run: mise run github:workflows:trogonstack-sync:pick-tag - name: Merge upstream tag id: merge if: steps.tag.outputs.up_to_date == 'false' env: TAG: ${{ steps.tag.outputs.tag }} - run: | - 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 - + run: mise run github:workflows:trogonstack-sync:merge-upstream - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 if: steps.tag.outputs.up_to_date == 'false' with: @@ -106,64 +62,17 @@ jobs: id: verify if: steps.tag.outputs.up_to_date == 'false' continue-on-error: true - run: | - go build ./... - go test ./... - + run: mise run github:workflows:trogonstack-sync:verify - name: Publish failing merge as a pull request if: steps.tag.outputs.up_to_date == 'false' && steps.verify.outcome == 'failure' env: TAG: ${{ steps.tag.outputs.tag }} - run: | - 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 - + run: mise run github:workflows:trogonstack-sync:publish-pull-request - name: Push main and build image if: steps.tag.outputs.up_to_date == 'false' && steps.verify.outcome == 'success' env: TAG: ${{ steps.tag.outputs.tag }} - run: | - # 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" - + run: mise run github:workflows:trogonstack-sync:push-main # Main may hold fork-only commits that were never released. The release # workflow is idempotent, so run it for HEAD when it has an image, and # build and release that commit when it has none. @@ -172,25 +81,4 @@ jobs: env: REGISTRY_TOKEN: ${{ github.token }} ACTOR: ${{ github.actor }} - run: | - head="$(git rev-parse HEAD)" - # The registry's anonymous token endpoint is not enough for a private - # package, so ask with the workflow token. - bearer="$(curl -fsS -u "$ACTOR:$REGISTRY_TOKEN" \ - "https://ghcr.io/token?service=ghcr.io&scope=repository:${IMAGE#ghcr.io/}:pull" | jq -r .token)" - status="$(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/$head")" - # 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 - gh workflow run trogonstack-release.yml --ref main - 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 + run: mise run github:workflows:trogonstack-sync:release-current diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000000..f42b6b3ce4f --- /dev/null +++ b/mise.toml @@ -0,0 +1,4 @@ +# Tooling for the trogonstack-* workflows only. Upstream's Go build does not +# read this file. Tasks live in .config/mise/tasks (ADR 6647967335). +[tools] +"aqua:google/go-containerregistry" = "0.22.1" From 8381197b90b07f612893bee8cc198ea650632dbc Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Nov 2026 16:43:52 -0500 Subject: [PATCH 3/4] fix(ci): trust the checkout's mise config on runners A fresh runner has never trusted this mise.toml, so mise run would stop at the trust check. Signed-off-by: Yordis Prieto --- .github/actions/setup-mise/action.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/actions/setup-mise/action.yml b/.github/actions/setup-mise/action.yml index 733932d2533..67881e7a4df 100644 --- a/.github/actions/setup-mise/action.yml +++ b/.github/actions/setup-mise/action.yml @@ -20,3 +20,5 @@ runs: 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" From 7c02472d923f781a841a644b5115b512d753f719 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Sat, 7 Nov 2026 16:46:28 -0500 Subject: [PATCH 4/4] fix(ci): stop releases on registry errors and pin the checked commit A failed crane ls or digest left the tag list incomplete, which could hide an earlier release of the commit or reuse a taken version number. A failed token request surfaced as a misleading 401. The weekly release resolved main again at run time, so it could release a commit other than the one whose image was checked. Signed-off-by: Yordis Prieto --- .config/mise/lib/ghcr.sh | 7 ++++++- .../workflows/trogonstack-release/add-calver-tag | 13 ++++++++----- .../workflows/trogonstack-sync/release-current | 3 ++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.config/mise/lib/ghcr.sh b/.config/mise/lib/ghcr.sh index 5d3fd191561..f414c3135e5 100644 --- a/.config/mise/lib/ghcr.sh +++ b/.config/mise/lib/ghcr.sh @@ -4,8 +4,13 @@ # 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 -r .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' \ diff --git a/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag b/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag index 64ff7c87f6f..64ee2b4cc20 100755 --- a/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag +++ b/.config/mise/tasks/github/workflows/trogonstack-release/add-calver-tag @@ -9,11 +9,14 @@ digest="$(crane digest "$IMAGE:$COMMIT" 2>/dev/null)" || { exit 1 } -pairs="$( - crane ls "$IMAGE" | grep -E '^[0-9]{4}\.[0-9]{3,4}\.[0-9]+$' | while read -r tag; do - echo "$tag $(crane digest "$IMAGE:$tag")" - done -)" || true +# 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)" diff --git a/.config/mise/tasks/github/workflows/trogonstack-sync/release-current b/.config/mise/tasks/github/workflows/trogonstack-sync/release-current index 366c05b74d5..f6c6c05c823 100755 --- a/.config/mise/tasks/github/workflows/trogonstack-sync/release-current +++ b/.config/mise/tasks/github/workflows/trogonstack-sync/release-current @@ -10,7 +10,8 @@ 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 - gh workflow run trogonstack-release.yml --ref main + # 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