Skip to content

slim-release-published #44

slim-release-published

slim-release-published #44

name: Slim Release Published
# Reconciles the stack catalog against supabase/slim-services releases. See
# docs/adr/0026-slim-artifact-mirrors.md, "Hotfix and upgrade pickup".
on:
repository_dispatch:
types:
- slim-release-published
permissions:
contents: read
concurrency:
group: slim-release-published-${{ github.event.client_payload.service }}
cancel-in-progress: false
# The default `single` cancels a pending run when another is queued, losing its release-visibility wait.
queue: max
jobs:
pickup:
runs-on: ubuntu-latest
# Setup (~10 min) + release visibility (<1 min) + the S3-mirror wait (up to ~10 min, normally only
# for the first planned item) + pin, render and push per item + the merge-queue wait (up to 30 min).
timeout-minutes: 75
permissions:
contents: read
steps:
- name: Checkout the default branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
# `Artifacts.ts` and `render-service-dockerfile.ts` import locked workspace dependencies.
- name: Setup
uses: ./.github/actions/setup
with:
dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }}
- name: Record the base commit
id: base
run: echo "sha=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT"
# Re-echoes a validate-payload failure so its `::error ::…` line surfaces as an annotation.
- name: Validate payload
id: validate
env:
SERVICE: ${{ github.event.client_payload.service }}
UPSTREAM_VERSION: ${{ github.event.client_payload.upstream_version }}
REVISION: ${{ github.event.client_payload.revision }}
RELEASE_VERSION: ${{ github.event.client_payload.release_version }}
run: |
set -euo pipefail
if ! output="$(bun .github/scripts/sync-artifacts-catalog.ts validate-payload \
--service "$SERVICE" --upstream "$UPSTREAM_VERSION" --revision "$REVISION" --release "$RELEASE_VERSION")"; then
echo "$output"
exit 1
fi
echo "$output" >>"$GITHUB_OUTPUT"
# `--expect-release` waits for the releases API to list the release behind this dispatch.
- name: Plan updates
id: plan
env:
SERVICE: ${{ steps.validate.outputs.service }}
RELEASE_VERSION: ${{ steps.validate.outputs.release_version }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
bun .github/scripts/sync-artifacts-catalog.ts plan-updates --service "$SERVICE" --format lines \
--expect-release "$RELEASE_VERSION" --output "${RUNNER_TEMP}/slim-updates.tsv"
count="$(wc -l < "${RUNNER_TEMP}/slim-updates.tsv" | tr -d ' ')"
echo "count=${count}" >>"$GITHUB_OUTPUT"
- name: Nothing to do
if: steps.plan.outputs.count == '0'
env:
SERVICE: ${{ steps.validate.outputs.service }}
RELEASE_VERSION: ${{ steps.validate.outputs.release_version }}
run: echo "No catalog updates for ${SERVICE} from ${RELEASE_VERSION}; nothing to do."
- name: Install regctl
if: steps.plan.outputs.count != '0'
run: |
set -euo pipefail
install -d "${RUNNER_TEMP}/regctl-bin"
curl -fsSLo "${RUNNER_TEMP}/regctl-bin/regctl" \
https://github.com/regclient/regclient/releases/download/v0.11.5/regctl-linux-amd64
echo "c93aa7638749f5aaac1a8e01787321889c78f0101809bb2880343478d0ba0467 ${RUNNER_TEMP}/regctl-bin/regctl" | sha256sum -c -
chmod +x "${RUNNER_TEMP}/regctl-bin/regctl"
echo "${RUNNER_TEMP}/regctl-bin" >>"$GITHUB_PATH"
"${RUNNER_TEMP}/regctl-bin/regctl" version
- name: Generate token
if: steps.plan.outputs.count != '0'
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
# One branch per planned item, rebuilt from the recorded base commit, never from a previous
# iteration's HEAD; `--release` pins the planned release, never "highest at apply time".
# `bun`/`pnpm` run with the tokens unset: only `git push` and `gh` ever see them.
# A merge-queued branch rejects pushes; see ADR 0026 "Hotfix and upgrade pickup" for the wait and hand-off.
- name: Apply planned updates
if: steps.plan.outputs.count != '0'
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_TOKEN: ${{ github.token }}
SERVICE: ${{ steps.validate.outputs.service }}
UPSTREAM_VERSION: ${{ steps.validate.outputs.upstream_version }}
REVISION: ${{ steps.validate.outputs.revision }}
RELEASE_VERSION: ${{ steps.validate.outputs.release_version }}
REPLAY: ${{ github.event.client_payload.replay }}
BASE_SHA: ${{ steps.base.outputs.sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
push_url="${PUSH_REMOTE_URL:-https://x-access-token:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git}"
manual_prefix="bun .github/scripts/sync-artifacts-catalog.ts --service ${SERVICE} --release"
in_merge_queue() {
# shellcheck disable=SC2016 # `$owner`, `$name` and `$branch` are GraphQL variables.
GH_TOKEN="$APP_TOKEN" gh api graphql \
-f query='query($owner: String!, $name: String!, $branch: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $branch, states: OPEN, first: 10) { nodes { isInMergeQueue isCrossRepository } } } }' \
-f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f branch="$1" \
--jq '[.data.repository.pullRequests.nodes[] | select(.isCrossRepository | not) | .isInMergeQueue] | any'
}
# Reads records on fd 3, not stdin, so commands the loop runs (`bun`, `gh`, `git`) never
# consume bytes meant for `read`.
while IFS=$'\x1f' read -r -u 3 kind branch title release from; do
if [ -z "$kind" ] || [ -z "$branch" ] || [ -z "$title" ] || [ -z "$release" ] || [ -z "$from" ]; then
echo "::error ::Malformed plan-updates line: kind='${kind}' branch='${branch}' title='${title}' release='${release}' from='${from}'."
exit 1
fi
git checkout -B "$branch" "$BASE_SHA"
env -u APP_TOKEN bun .github/scripts/sync-artifacts-catalog.ts --service "$SERVICE" --release "$release"
env -u APP_TOKEN -u GITHUB_TOKEN pnpm exec oxfmt --config .oxfmtrc.json packages/stack/src/Artifacts.ts
env -u APP_TOKEN -u GITHUB_TOKEN bun apps/cli/scripts/render-service-dockerfile.ts
generated_files=(
packages/stack/src/Artifacts.ts
apps/cli/src/shared/services/Dockerfile
apps/cli-go/pkg/config/templates/Dockerfile
)
if git diff --quiet -- "${generated_files[@]}"; then
echo "${branch}: catalog and Dockerfiles already match ${release}; skipping."
continue
fi
git add -- "${generated_files[@]}"
git commit -m "$title"
if ! push_output="$(git push --force "$push_url" "HEAD:refs/heads/${branch}" 2>&1)"; then
echo "$push_output"
if ! grep -q "queued for merging cannot be updated" <<<"$push_output"; then
echo "::error ::Failed to push ${branch}. Run manually: ${manual_prefix} ${release}, regenerate the Dockerfiles with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand."
exit 1
fi
manual_steps="run manually: ${manual_prefix} ${release}, regenerate the Dockerfiles with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand."
queued_steps="Once ${branch} leaves the merge queue, ${manual_steps}"
# The app token minted just before this step expires after one hour; capping the wait at
# 50 minutes into the step leaves room for the lookups and the dispatch that follow it.
deadline=$((SECONDS + 1800 < 3000 ? SECONDS + 1800 : 3000))
while :; do
if ! queued="$(in_merge_queue "$branch")"; then
echo "::error ::Failed to read the merge-queue state of ${branch}. ${queued_steps}"
exit 1
fi
case "$queued" in
false) break ;;
true) ;;
*)
echo "::error ::Unexpected merge-queue state '${queued}' for ${branch}. ${queued_steps}"
exit 1
;;
esac
if [ "$SECONDS" -ge "$deadline" ]; then
echo "::error ::${branch} is still in the merge queue. ${queued_steps}"
exit 1
fi
sleep 30
done
# `replay` counts consecutive re-sends; anything but 0-2 stops the chain.
if ! [[ "${REPLAY:-0}" =~ ^[0-2]$ ]]; then
echo "::error ::Stopped re-dispatching after repeated merge-queue rejections of ${branch}; ${manual_steps}"
exit 1
fi
if ! GH_TOKEN="$APP_TOKEN" gh api "repos/${GITHUB_REPOSITORY}/dispatches" \
-f event_type=slim-release-published \
-f "client_payload[service]=${SERVICE}" \
-f "client_payload[upstream_version]=${UPSTREAM_VERSION}" \
-f "client_payload[revision]=${REVISION}" \
-f "client_payload[release_version]=${RELEASE_VERSION}" \
-f "client_payload[replay]=$((${REPLAY:-0} + 1))"; then
echo "::error ::Failed to re-dispatch slim-release-published for ${SERVICE} ${RELEASE_VERSION}. Re-run this job."
exit 1
fi
echo "::warning ::${branch} was held by a merge-queued pull request; re-dispatched slim-release-published so a fresh run re-plans from ${DEFAULT_BRANCH}."
exit 0
fi
echo "$push_output"
if [ "$kind" = "upgrade" ]; then
action="bumps"
else
action="pins"
fi
# Names the release this PR actually pins, not necessarily RELEASE_VERSION: postgres
# can plan a hotfix on one line and an unrelated upgrade on another in one dispatch.
body="$(printf 'This %s %s from %s to %s.\n\nhttps://github.com/supabase/slim-services/releases/tag/%s-%s\n\nPlanned after supabase/slim-services published %s. This branch is rewritten from %s whenever a newer relevant release arrives.\n' \
"$action" "$SERVICE" "$from" "$release" "$SERVICE" "$release" "$RELEASE_VERSION" "$DEFAULT_BRANCH")"
# `--head` matches by branch name only and ignores the owner, so a fork PR with the
# same head branch name would otherwise match too; `isCrossRepository` excludes it.
existing="$(GH_TOKEN="$APP_TOKEN" gh pr list --head "$branch" --base "$DEFAULT_BRANCH" --state open --json number,isCrossRepository --jq 'map(select(.isCrossRepository | not)) | .[0].number // empty')"
if [ -n "$existing" ]; then
if ! GH_TOKEN="$APP_TOKEN" gh pr edit "$existing" --title "$title" --body "$body"; then
echo "::error ::Pushed ${branch} but failed to edit pull request #${existing}. Run manually: ${manual_prefix} ${release}, then edit the pull request from ${branch} by hand."
exit 1
fi
elif ! GH_TOKEN="$APP_TOKEN" gh pr create \
--title "$title" \
--body "$body" \
--head "$branch" \
--base "$DEFAULT_BRANCH"; then
echo "::error ::Pushed ${branch} but failed to open a pull request. Run manually: ${manual_prefix} ${release}, then open a pull request from ${branch} against ${DEFAULT_BRANCH} by hand."
exit 1
fi
done 3< "${RUNNER_TEMP}/slim-updates.tsv"