Skip to content

fix(mcp): return the created branch when the branches_create wait fails #4200

fix(mcp): return the created branch when the branches_create wait fails

fix(mcp): return the created branch when the branches_create wait fails #4200

Workflow file for this run

# Build, smoke-test, and (for non-PR events) publish the Agor container image.
#
# Builds the `production-source` target of docker/Dockerfile and, on non-PR
# events, pushes it to Docker Hub (Buildx + GHA cache + docker/metadata tags).
#
# `production-source` builds agor-live from THIS checkout (turbo build via
# packages/agor-live/build.sh), so the image is pinned to the built commit
# rather than `agor-live@latest` on npm.
#
# Publish model: pull requests build and smoke-test the production image from
# their checkout without authenticating to Docker Hub, publishing an image, or
# exporting an untrusted PR-scoped cache. They can restore the trusted cache
# produced by non-PR events. A successful CI run for a main push first publishes
# :<full-sha>, then a serialized promotion may move :main and :latest. Those
# mutable tags always identify a successfully tested main image; when a newer
# main commit is red, they remain on the last successful promotion. v* tags
# and manual runs publish directly, so those trusted commits remain deployable
# by their own SHA. The preview dependency image is published only by successful
# main workflow_run builds, and promoted under the same serialized head check.
#
# Pushes to Docker Hub (docker.io/preset/agor). Required repo secrets
# (Settings → Secrets and variables → Actions):
# DOCKERHUB_USERNAME — Docker Hub account with push access to `preset`
# DOCKERHUB_TOKEN — Docker Hub access token (Read/Write) for that account
name: Build image
on:
workflow_run:
workflows: [CI]
types: [completed]
branches: [main]
push:
tags: ['v*']
pull_request:
branches: [main]
workflow_dispatch:
permissions:
contents: read
# docker.io is docker/login-action's default registry, so no `registry:` needed.
env:
IMAGE: preset/agor
# The commit we build, stamp, and tag: the PR merge on pull_request events;
# the tested main commit on workflow_run events; otherwise the pushed commit.
# PR workflows come from the merge ref. Checking out only the head mixes a
# newer workflow with older source (e.g. newly added Docker targets missing).
# Keeps the checkout, the :<sha> tag, and the in-app build banner in lockstep.
IMAGE_REVISION: ${{ github.event.workflow_run.head_sha || github.sha }}
jobs:
build:
name: Build & push
# Branch filtering applies to both push- and PR-triggered CI runs. Only a
# successful push run is allowed to publish main; PRs keep using this
# workflow's direct pull_request trigger instead.
if: >-
github.event_name != 'workflow_run' ||
(
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success'
)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
timeout-minutes: 45
concurrency:
group: build-image-${{ github.event.pull_request.number || github.event.workflow_run.head_sha || github.ref }}
cancel-in-progress: true
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# Build the exact tested/main, PR-merge, or tag commit so image
# contents match the :<sha> tag.
ref: ${{ env.IMAGE_REVISION }}
- name: Validate image publication policy
run: node scripts/check-image-publication-policy.mjs
- name: Select Railway image validation
id: railway
uses: actions/github-script@v9
with:
script: |
const { shouldBuildRailwayImage } = await import(`${process.env.GITHUB_WORKSPACE}/scripts/railway-image-inputs.mjs`);
const build = await shouldBuildRailwayImage({ github, context });
core.setOutput('build', String(build));
core.notice(build ? 'Validating Railway dependency image' : 'Skipping Railway image: no runtime/dependency inputs changed');
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
# Pull requests never need registry credentials: every PR validates the
# production image locally, including forks and Dependabot.
- name: Log in to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Build once into the local Docker store so it can be smoke-tested before
# anything is published. Pull requests restore the trusted GHA cache but
# do not export a PR-scoped cache: the source layer changes per commit and
# measured exports cost more time than they save.
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
target: production-source
platforms: linux/amd64
# Real SHA for the in-app build banner (.git is dockerignored, so the
# daemon's stamp script can't derive it itself).
build-args: |
AGOR_BUILD_SHA=${{ env.IMAGE_REVISION }}
load: true
tags: ${{ env.IMAGE }}:smoke
cache-from: type=gha,scope=agor-image
cache-to: ${{ github.event_name != 'pull_request' && 'type=gha,mode=max,scope=agor-image' || '' }}
# Boot the container and require a healthy daemon: exercises the deployed
# tree end to end (entrypoint init, db migrate, @agor/* symlink
# resolution, native modules) — catches deploy omissions the build alone
# can't see.
- name: Smoke test
run: |
docker run -d --name agor-smoke -p 3030:3030 \
-e AGOR_JWT_SECRET=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
-e AGOR_MASTER_SECRET=abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 \
${{ env.IMAGE }}:smoke
for i in $(seq 1 60); do
if curl -fsS http://localhost:3030/health > /dev/null 2>&1; then
echo "Daemon healthy."
docker rm -f agor-smoke
exit 0
fi
sleep 2
done
echo "::error::Daemon did not become healthy within 120s"
docker logs agor-smoke
exit 1
# Only runtime/dependency changes need this additional PR validation.
# Main/tag/manual runs keep validating; PRs never publish images/caches.
- name: Build preview runtime
if: steps.railway.outputs.build == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
target: railway-preview
platforms: linux/amd64
load: true
tags: ${{ env.IMAGE }}:preview-smoke
cache-from: type=gha,scope=agor-preview
cache-to: ${{ github.event_name == 'workflow_run' && 'type=gha,mode=max,scope=agor-preview' || '' }}
- name: Smoke test preview runtime
if: steps.railway.outputs.build == 'true'
run: |
docker run --rm --entrypoint sh ${{ env.IMAGE }}:preview-smoke -ec '
test "$(node /usr/local/lib/agor/runtime-checkout.mjs fingerprint)" = "$(cat /opt/agor-dependency-fingerprint)"
test -x /usr/local/bin/docker-entrypoint-runtime.sh
node --check /usr/local/lib/agor/runtime-watch.mjs
cd /app/packages/git
node -e "require(\"simple-git\")"
'
# Use the loaded image via Docker's local builder; no registry push/pull.
# These are build checks, not deliverables. Validate inside the final
# stage instead of exporting/loading two more large dependency images.
- name: Test warm preview base
if: steps.railway.outputs.build == 'true'
timeout-minutes: 10
run: |
set -euo pipefail
backup="$(mktemp)"
dockerfile="$(mktemp)"
cp package.json "$backup"
trap 'cp "$backup" package.json; rm -f "$backup" "$dockerfile"' EXIT
cp docker/Dockerfile "$dockerfile"
cat >> "$dockerfile" <<'DOCKERFILE'
FROM railway-preview AS railway-preview-checked
RUN test "$(node /usr/local/lib/agor/runtime-checkout.mjs fingerprint)" = "$(cat /opt/agor-dependency-fingerprint)" \
&& cd /app/packages/git \
&& node -e 'require("simple-git")'
DOCKERFILE
docker buildx build --builder default --output=type=cacheonly --progress=plain \
--build-arg AGOR_PREVIEW_BASE=${{ env.IMAGE }}:preview-smoke \
--target railway-preview-checked -f "$dockerfile" . 2>&1 | tee /tmp/preview-warm.log
grep -E '^#[0-9]+ [0-9.]+ Reusing preview dependencies$' /tmp/preview-warm.log
# A manifest-only metadata change invalidates the fingerprint without
# changing the lockfile resolution. This must run the frozen install.
node -e 'const fs = require("node:fs"); const p = JSON.parse(fs.readFileSync("package.json")); p.description = "preview dependency fingerprint test"; fs.writeFileSync("package.json", JSON.stringify(p));'
docker buildx build --builder default --output=type=cacheonly --progress=plain \
--build-arg AGOR_PREVIEW_BASE=${{ env.IMAGE }}:preview-smoke \
--target railway-preview-checked -f "$dockerfile" . 2>&1 | tee /tmp/preview-changed.log
grep -E '^#[0-9]+ [0-9.]+ Installing changed preview dependencies$' /tmp/preview-changed.log
- name: Push preview runtime
if: github.event_name == 'workflow_run'
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
target: railway-preview
platforms: linux/amd64
push: true
tags: ${{ env.IMAGE }}:preview-runtime-${{ env.IMAGE_REVISION }}
cache-from: type=gha,scope=agor-preview
- name: Docker metadata
if: github.event_name != 'pull_request'
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE }}
# Full commit SHA on every build; PR ref and semver tags where
# applicable. Mutable main tags are promoted in the serialized job.
flavor: latest=false
tags: |
type=raw,value=${{ env.IMAGE_REVISION }}
type=semver,pattern={{version}}
labels: |
org.opencontainers.image.title=agor
org.opencontainers.image.revision=${{ env.IMAGE_REVISION }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
# Publish trusted non-PR events after the smoke test. Keep the historical
# job/check name for branch-protection and dashboard compatibility.
- name: Push image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile
target: production-source
platforms: linux/amd64
build-args: |
AGOR_BUILD_SHA=${{ env.IMAGE_REVISION }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=agor-image
# Mutable tags are a separate, serialized promotion. Immutable SHA builds
# can proceed independently, while main/latest can never be pushed by two
# workflow_run jobs at once. Keep the full queue: builds may finish out of
# order, and dropping the one current-head promotion could strand the tags.
promote-main:
name: Promote main image
needs: build
if: github.event_name == 'workflow_run' && needs.build.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 5
concurrency:
group: build-image-main-promotion
queue: max
steps:
# Most queued jobs are stale when main advances quickly. Reject them
# before installing Buildx or opening an authenticated registry session.
- name: Preflight current main head
id: preflight
run: |
current_sha="$(git ls-remote "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" refs/heads/main | cut -f1)"
if [[ "$current_sha" == "$IMAGE_REVISION" ]]; then
echo "current=true" >> "$GITHUB_OUTPUT"
else
echo "current=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping stale promotion because main is now $current_sha"
fi
- name: Set up Docker Buildx
if: steps.preflight.outputs.current == 'true'
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
if: steps.preflight.outputs.current == 'true'
uses: docker/login-action@v4.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# Keep the head check and registry mutation in one serialized step. A
# stale queued run skips; if main advances after this check, the tags
# intentionally remain on a tested image until a newer run passes CI.
- name: Promote tested main image
if: steps.preflight.outputs.current == 'true'
run: |
current_sha="$(git ls-remote "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" refs/heads/main | cut -f1)"
if [[ "$current_sha" != "$IMAGE_REVISION" ]]; then
echo "::notice::Skipping mutable tags because main is now $current_sha"
exit 0
fi
docker buildx imagetools create \
--tag "${IMAGE}:main" \
--tag "${IMAGE}:latest" \
"${IMAGE}:${IMAGE_REVISION}"
docker buildx imagetools create \
--tag "${IMAGE}:preview-runtime-main" \
"${IMAGE}:preview-runtime-${IMAGE_REVISION}"