Repository navigation
fix(mcp): return the created branch when the branches_create wait fails #4200
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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}" |