feat(mt#4974): Ship a package-resident product rule corpus and scaffo… #562
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
| name: Deploy Reviewer | |
| # mt#3117 — CI-owned release-phase deploy for the `minsky-reviewer-webhook` | |
| # service, replacing Railway's native branch-wide auto-trigger (a Railway | |
| # source-build that redeployed on EVERY push to main matching | |
| # services/reviewer/railway.json's now-retired `build.watchPatterns`). | |
| # | |
| # Clones the STRUCTURE of `deploy-minsky-mcp.yml` (mt#2104/mt#2505): build | |
| # -> smoke -> migrate -> push image -> explicit Railway redeploy. The | |
| # reviewer is converted from a repo-source Dockerfile build to an | |
| # image-source deploy (ghcr.io/edobry/minsky-reviewer), the same shape | |
| # minsky-mcp already uses — see services/reviewer/deploy.config.ts, | |
| # services/reviewer/railway.json (retired), and the `reviewerService` | |
| # resource in infra/index.ts. | |
| # | |
| # Migrate-before-push is the load-bearing gate (identical mechanism to | |
| # deploy-minsky-mcp.yml): the migrate step is FATAL — a failing migration | |
| # means the image is never pushed, so nothing new is ever deployed. The | |
| # previously-deployed reviewer keeps serving. | |
| # | |
| # mt#3180 CORRECTION (2026-07-25): this workflow originally treated the | |
| # explicit "Trigger Railway redeploy" step below as belt-and-suspenders — | |
| # non-fatal, on the claim that Railway's image-source auto-redeploy-on-push | |
| # (the same mechanism deploy-minsky-mcp.yml cites for minsky-mcp, mt#2342) | |
| # would independently pick up every new :latest push. That claim was NOT | |
| # actually verified for THIS service, and turned out to be false: across | |
| # four consecutive main-branch image pushes on 2026-07-24, the live | |
| # reviewer deployment never advanced (frozen at the operator's 17:27 | |
| # image-source flip) — auto-redeploy-on-push does not fire for | |
| # minsky-reviewer-webhook. The explicit redeploy step is therefore the | |
| # ACTUAL deploy trigger for this service, not a backstop. | |
| # | |
| # It was also broken independently: the step hand-rolled a GraphQL | |
| # `serviceInstanceRedeploy` mutation authenticated with a plain | |
| # `Authorization: Bearer $RAILWAY_TOKEN` header, which returned | |
| # `Not Authorized` (CI run 30122524391) — `RAILWAY_TOKEN` is a Railway | |
| # PROJECT-scoped token, and the public GraphQL API does not accept a | |
| # project token via a bare Bearer header the way it accepts an account | |
| # token. The official `railway` CLI handles project-token auth correctly | |
| # (`railway redeploy --from-source` succeeded live against this exact | |
| # service on 2026-07-25) and now replaces the hand-rolled GraphQL call. See | |
| # mt#3180 for the full incident, root cause, and fix. | |
| # | |
| # Runs migration against the reviewer's OWN migration tree | |
| # (services/reviewer/migrations/pg, tracked via | |
| # drizzle.__drizzle_migrations_reviewer — mt#1967), NOT the main domain | |
| # tree deploy-minsky-mcp.yml migrates. See | |
| # services/reviewer/scripts/migrate.ts for the standalone entrypoint this | |
| # step invokes (services/reviewer/src/db/migrate.ts is only reachable via | |
| # applyMigrations() at server boot otherwise, and that boot-time call is | |
| # left in place by this task — dual application is safe: the drizzle | |
| # ledger is idempotent and high-water-mark based). | |
| # | |
| # `paths:` mirrors services/reviewer/railway.json's retired | |
| # `build.watchPatterns` (this is now the SINGLE source of the reviewer's | |
| # build closure — the config-as-code watchPatterns mechanism this list used | |
| # to drive no longer applies once the service is image-source). Explicitly | |
| # names services/reviewer/migrations/** even though services/reviewer/** | |
| # already covers it, so the migration tree's presence in the trigger scope | |
| # is visible without cross-referencing the retired file. | |
| # | |
| # Prerequisites (one-time setup, OPERATOR follow-up — see | |
| # services/reviewer/DEPLOY.md): | |
| # 1. RAILWAY_REVIEWER_TOKEN (a Railway PROJECT token scoped to | |
| # minsky-reviewer / production) is NOT YET SET as a GitHub Actions | |
| # secret (mt#3251, operator-only — minting a project token is a | |
| # dashboard action with no CLI/API path). Until it exists, the redeploy | |
| # step below falls back to the generic secrets.RAILWAY_TOKEN, which is | |
| # scoped to a DIFFERENT project and is expected to keep failing — see | |
| # "Trigger Railway redeploy" below and services/reviewer/DEPLOY.md. | |
| # MINSKY_PERSISTENCE_POSTGRES_URL is already provisioned (reused from | |
| # deploy-minsky-mcp.yml; verified as of the mt#3117 spec's "External | |
| # preconditions" check). | |
| # 2. Railway `minsky-reviewer-webhook` service must be switched from | |
| # repo+Dockerfile source to Docker Image source | |
| # (ghcr.io/edobry/minsky-reviewer:latest) via the dashboard (Settings > | |
| # Source > Docker Image) — the same one-time flip minsky-mcp already | |
| # has (deploy-minsky-mcp.yml's own prerequisite note). This is a live | |
| # Railway mutation and is NOT performed by this PR — it happens after | |
| # merge, once this workflow has produced a first `:latest` image to | |
| # point the service at. | |
| # 3. GHCR package visibility for `minsky-reviewer` set to match repo | |
| # visibility, same as the `minsky` package. | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "services/reviewer/**" | |
| - "services/reviewer/migrations/**" | |
| - "packages/shared/src/**" | |
| - "packages/shared/package.json" | |
| - "packages/shared/tsconfig.json" | |
| - "packages/domain/src/**" | |
| - "packages/domain/package.json" | |
| - "packages/domain/tsconfig.json" | |
| - "services/site/package.json" | |
| - "services/cockpit/package.json" | |
| - "package.json" | |
| - "bun.lock" | |
| - "bunfig.toml" | |
| - "tsconfig.json" | |
| - ".github/workflows/deploy-reviewer.yml" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| packages: write | |
| # Only one deploy at a time, same rationale as deploy-minsky-mcp.yml | |
| # (mt#2505): this job runs a prod migration step, and cancelling mid-flight | |
| # could leave held locks or a partially-applied migration set. | |
| concurrency: | |
| group: deploy-reviewer-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| IMAGE: ghcr.io/edobry/minsky-reviewer | |
| # Secondary hedge for the mt#3623 install flake: disables the streaming | |
| # tarball path implicated upstream (oven-sh/bun#34821; fix PR #34827 is still | |
| # unmerged, so no release carries a fix). Measured at 0 failures in 10 | |
| # cold-cache installs with the flag set, against a 3-in-10 unmitigated | |
| # baseline -- mt#3623 records both runs and the method. The per-step retry | |
| # loops remain the load-bearing mitigation. | |
| # Remove both when a bun release carries the upstream fix. | |
| BUN_FEATURE_FLAG_DISABLE_STREAMING_INSTALL: "1" | |
| jobs: | |
| build-migrate-deploy: | |
| name: build-migrate-deploy | |
| runs-on: ubuntu-latest | |
| # Throwaway Postgres for the smoke-boot step ONLY — never touched by | |
| # the "Apply reviewer migrations to production" step below, which uses | |
| # secrets.MINSKY_PERSISTENCE_POSTGRES_URL instead. Distinct in kind | |
| # from mt#1557 (pre-merge/PR-time container smoke against a throwaway | |
| # Postgres): this runs at release time (push to main only, no | |
| # `pull_request:` trigger above) and its job is "does the just-built | |
| # image boot and serve /health at all", not PR-time regression | |
| # prevention. The reviewer's boot path calls applyMigrations() | |
| # unconditionally before the HTTP server starts (server.ts, | |
| # `if (import.meta.main)` — unchanged by this task), so a real boot | |
| # smoke requires SOME reachable Postgres; this ephemeral instance is | |
| # discarded at job end and never holds prod data. | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: reviewer_smoke | |
| POSTGRES_PASSWORD: reviewer_smoke | |
| POSTGRES_DB: reviewer_smoke | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 5s | |
| --health-timeout 5s | |
| --health-retries 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Build Docker image | |
| run: docker build -t $IMAGE:ci -f services/reviewer/Dockerfile . | |
| - name: Boot container and probe /health (throwaway Postgres) | |
| env: | |
| # Test-only credentials scoped to this job's ephemeral postgres | |
| # service container, no access outside it. Composed from parts, | |
| # with the URL scheme ALSO kept out of the literal source line | |
| # (gitleaks' credential-URL rule matches on a literal scheme | |
| # token immediately followed by "://" — see .gitleaks.toml's | |
| # database-url-credentials regex — so the scheme is a variable | |
| # too, not just the user/password). Same convention in spirit as | |
| # cold-start-migrate.yml's DATABASE_URL, structured so it never | |
| # produces a matching literal instead of relying on an allowlist | |
| # entry (out of this task's scope to add). | |
| SMOKE_PG_SCHEME: postgres | |
| SMOKE_PG_USER: reviewer_smoke | |
| SMOKE_PG_PASSWORD: reviewer_smoke | |
| run: | | |
| set -euo pipefail | |
| PORT=$(( 30000 + RANDOM % 20000 )) | |
| echo "Boot port: $PORT" | |
| SMOKE_PG_URL="${SMOKE_PG_SCHEME}://${SMOKE_PG_USER}:${SMOKE_PG_PASSWORD}@127.0.0.1:5432/reviewer_smoke" | |
| # --network host puts this container on the runner's network | |
| # namespace directly, so 127.0.0.1:5432 reaches the `postgres` | |
| # service declared above (GH Actions maps job services' declared | |
| # `ports:` onto the runner's localhost, not a docker-internal | |
| # network reachable from a separately-launched container). | |
| # NOT `--rm` (mt#4136): the failure branch below reads this | |
| # container's logs, and `--rm` reaps it the moment it EXITS — so in | |
| # the case that matters most, a container that CRASHED rather than | |
| # one that hung, `docker logs` printed the daemon's own | |
| # "No such container" error instead of the boot output, and `|| true` | |
| # kept that from being loud. The cleanup trap removes the container | |
| # instead, after the logs have been read. | |
| docker run -d --name reviewer-smoke --network host \ | |
| -e PORT=$PORT \ | |
| -e MINSKY_REVIEWER_APP_ID=1 \ | |
| -e MINSKY_REVIEWER_PRIVATE_KEY=smoke-test-key \ | |
| -e MINSKY_REVIEWER_INSTALLATION_ID=1 \ | |
| -e MINSKY_REVIEWER_WEBHOOK_SECRET=smoke-test-secret \ | |
| -e REVIEWER_PROVIDER=openai \ | |
| -e OPENAI_API_KEY=smoke-test-key \ | |
| -e MINSKY_PERSISTENCE_POSTGRES_URL="$SMOKE_PG_URL" \ | |
| $IMAGE:ci | |
| cleanup() { | |
| docker rm -f reviewer-smoke 2>/dev/null || true | |
| } | |
| trap cleanup EXIT | |
| DEADLINE=$(( $(date +%s) + 60 )) | |
| while [ "$(date +%s)" -lt $DEADLINE ]; do | |
| if curl -fsS "http://127.0.0.1:$PORT/health" > /tmp/health.json 2>/dev/null; then | |
| echo "/health responded within deadline:" | |
| cat /tmp/health.json | |
| echo | |
| exit 0 | |
| fi | |
| sleep 0.5 | |
| done | |
| echo "/health did not respond 200 within 60s" | |
| # Pre-declared so the fallback's `$SMOKE_STATE` is unambiguously bound | |
| # under `set -u`. Bash does assign the variable from a FAILED command | |
| # substitution before `||` runs, so this is belt-and-braces rather than | |
| # a bug fix — but the two-step form states that intent instead of | |
| # asking every reader to know it. | |
| SMOKE_STATE="" | |
| SMOKE_STATE="$(docker inspect -f '{{.State.Status}} (exit {{.State.ExitCode}})' reviewer-smoke 2>&1)" \ | |
| || SMOKE_STATE="inspect failed: $SMOKE_STATE" | |
| echo "Container state: $SMOKE_STATE" | |
| # Captured, not piped straight to tail: an EMPTY log and a log this | |
| # step failed to read are different findings, and printing nothing | |
| # for both is what made the previous version unreadable. | |
| SMOKE_LOGS="$(docker logs reviewer-smoke 2>&1 | tail -80 || true)" | |
| if [ -n "$SMOKE_LOGS" ]; then | |
| echo "--- reviewer-smoke logs (last 80 lines) ---" | |
| echo "$SMOKE_LOGS" | |
| else | |
| echo "--- reviewer-smoke produced NO log output (container wrote nothing before failing) ---" | |
| fi | |
| exit 1 | |
| - name: Setup Bun | |
| if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.14" | |
| - name: Install dependencies | |
| if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' | |
| run: for i in 1 2 3; do if bun install --frozen-lockfile --no-progress; then break; fi; if [ "$i" = 3 ]; then exit 1; fi; echo "bun install failed (mt#3623 tarball flake) - retry $i"; sleep 5; done | |
| # mt#3117 — deploy-keyed, single-runner PROD migration for the | |
| # reviewer's OWN tree. Runs BEFORE the GHCR push, so a failure here | |
| # aborts the job before any image is pushed — Railway's image-source | |
| # auto-redeploy has nothing new to deploy, and the prior version keeps | |
| # serving against the unchanged schema (Heroku-release-phase | |
| # semantics — identical mechanism to deploy-minsky-mcp.yml's own | |
| # migrate step, mt#2505). | |
| # | |
| # SECURITY: gated to push/workflow_dispatch on main; this workflow has | |
| # no `pull_request:` trigger at all, so the mt#1477 pwn-request class | |
| # does not apply. NEVER add a pull_request_target trigger here. | |
| - name: Apply reviewer migrations to production | |
| if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' | |
| env: | |
| # DDL-capable `postgres` role — the SAME CI secret | |
| # deploy-minsky-mcp.yml already consumes for the main domain tree | |
| # (verified provisioned, mt#3117 spec's External preconditions). | |
| # Deliberately NOT added to the reviewer's Railway runtime | |
| # environment — that is exactly the credential boundary this task | |
| # exists to establish (see the spec's Authoritative-source check | |
| # on why Railway's preDeployCommand was rejected). | |
| MINSKY_PERSISTENCE_POSTGRES_URL: ${{ secrets.MINSKY_PERSISTENCE_POSTGRES_URL }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${MINSKY_PERSISTENCE_POSTGRES_URL:-}" ]; then | |
| echo "::error::MINSKY_PERSISTENCE_POSTGRES_URL secret not set — refusing to deploy." | |
| echo "Add the prod Postgres connection as a repo secret to enable the deploy-keyed" | |
| echo "reviewer migration (mt#3117). Failing the job avoids shipping new reviewer" | |
| echo "code against an unmigrated schema." | |
| exit 1 | |
| fi | |
| set +e | |
| timeout 600 bun run services/reviewer/scripts/migrate.ts | |
| rc=$? | |
| set -e | |
| if [ "$rc" -ne 0 ]; then | |
| if [ "$rc" -eq 124 ]; then | |
| echo "::error::reviewer prod migration timed out after 600s — failing the deploy (no image pushed)." | |
| else | |
| echo "::error::reviewer prod migration failed (exit $rc) — failing the deploy (no image pushed)." | |
| fi | |
| exit "$rc" | |
| fi | |
| echo "Reviewer prod migration step completed (exit 0)." | |
| - name: Login to GHCR | |
| if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Tag and push image | |
| if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' | |
| run: | | |
| SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) | |
| docker tag $IMAGE:ci $IMAGE:latest | |
| docker tag $IMAGE:ci $IMAGE:sha-${SHORT_SHA} | |
| # mt#3979: `docker push` to GHCR intermittently ends in `unknown blob` | |
| # after most layers have already pushed — a transient registry | |
| # condition that only a re-run recovered. The wrapper retries ONLY the | |
| # conditions enumerated below and fails fast on everything else, so a | |
| # real failure stays loud. The immutable sha tag goes first so a | |
| # failure cannot leave the mutable :latest pointer promoted while the | |
| # run goes red. | |
| # | |
| # The set is enumerated HERE so a reader of the deploy step can see | |
| # what it is allowed to absorb without opening another file. The | |
| # script owns it; this copy is ASSERTED against the script by | |
| # scripts/ci/docker-push-with-retry.test.ts, so adding a condition | |
| # there without adding it here fails the suite. A duplicate that is | |
| # checked is not a second source of truth. | |
| # transient-retried: unknown blob; blob upload unknown; BLOB_UPLOAD_UNKNOWN; a standalone EOF; connection reset by peer; i/o timeout; TLS handshake timeout; cannot reuse body, request must be retried; registry 5xx status; registry 5xx response body | |
| # transient-not-retried: unauthorized; denied; manifest invalid; name unknown; toomanyrequests (429); anything unrecognised | |
| ./scripts/ci/docker-push-with-retry.sh "$IMAGE:sha-${SHORT_SHA}" "$IMAGE:latest" | |
| echo "Pushed (in this order): $IMAGE:sha-${SHORT_SHA}, $IMAGE:latest" | |
| # mt#3180 (2026-07-25) — this step is the ACTUAL deploy trigger for | |
| # minsky-reviewer-webhook, not a backstop (Railway's image-source | |
| # auto-redeploy-on-push does not fire for this service — verified | |
| # across four consecutive pushes that never advanced the live | |
| # deployment). Installs the official `railway` CLI (railwayapp/cli, | |
| # via the `@railway/cli` npm package — the same install | |
| # `cockpit-preview.yml` uses) IN THIS SAME STEP as the redeploy call | |
| # itself, so there is no cross-step PATH-propagation ambiguity to | |
| # reason about. FATAL: a false-green report here is worse than an | |
| # honest failure — a non-fatal version of this step produced FOUR | |
| # false-green runs (2026-07-25T21:57-23:05Z) where nothing deployed. | |
| # | |
| # mt#3223 (2026-07-25) — first CLI fix attempt. Passed the secret as | |
| # `RAILWAY_TOKEN` and failed with `Service '3913e8a4-…' not found` (run | |
| # 30178827355) — a context-resolution failure, not a permission one. | |
| # mt#3223 then tried to work around it by passing the secret as | |
| # `RAILWAY_API_TOKEN` (account scope) instead, plus an explicit | |
| # `railway link --project/--environment/--service` to establish | |
| # project context in the unlinked CI checkout, plus a `whoami` | |
| # diagnostic. Both `whoami` and `link` kept failing `Unauthorized` — | |
| # `secrets.RAILWAY_TOKEN` is neither a valid ACCOUNT token for this | |
| # workspace nor scoped to minsky-reviewer, because it is a Railway | |
| # PROJECT token scoped to a DIFFERENT project (the same generic | |
| # secret is also consumed by deploy-minsky-mcp.yml and | |
| # post-deploy-health-monitor.yml, so it cannot simply be re-scoped to | |
| # this one service). mt#3223 improved the diagnostics and made the | |
| # failure legible; it did not and could not fix the credential. | |
| # | |
| # mt#3251 (2026-07-28) — CORRECTED FIX: adopt the per-project-secret | |
| # precedent already established by `cockpit-preview.yml` (:52, :157) | |
| # exactly, instead of working around the wrong credential. A Railway | |
| # PROJECT token carries its OWN project+environment context, which is | |
| # precisely why cockpit-preview passes it as `RAILWAY_TOKEN` with NO | |
| # `railway link` step. (An earlier version of this comment claimed | |
| # cockpit-preview.yml proves the IDENTICAL generic-token mechanism | |
| # works in this repo's CI — that was wrong; cockpit passes its OWN | |
| # project token, `RAILWAY_COCKPIT_TOKEN`, never the shared generic | |
| # one. This step now FOLLOWS that same per-project-secret shape | |
| # rather than merely describing it.) The mt#3223 `railway link` step | |
| # is therefore REMOVED below — it is unnecessary once the token IS | |
| # project-scoped. `whoami` is kept, but only as a NON-FATAL | |
| # diagnostic (unchanged from mt#3223): it helps distinguish | |
| # "token invalid" from "redeploy failed for another reason" if this | |
| # step ever fails again. | |
| # | |
| # PREFER/FALLBACK, not a hard swap: `RAILWAY_REVIEWER_TOKEN` is a new | |
| # secret that does NOT exist yet as of this change — minting a | |
| # Railway project token is an operator-only dashboard action with no | |
| # CLI/API path (probed and confirmed 2026-07-28; see | |
| # services/reviewer/DEPLOY.md). A hard swap to `RAILWAY_REVIEWER_TOKEN` | |
| # alone would turn today's "Unauthorized" failure into a "secret not | |
| # set" failure — still red, and fixing nothing until the operator | |
| # acts. Instead this step PREFERS `RAILWAY_REVIEWER_TOKEN` and FALLS | |
| # BACK to the existing generic `RAILWAY_TOKEN` secret, so the deploy | |
| # keeps its current (broken) behavior until the operator adds the new | |
| # secret, and starts working the moment they do — no second PR | |
| # required. | |
| # | |
| # The fallback is resolved in the SHELL (bash `${VAR:-default}`), not | |
| # via a `${{ secrets.A || secrets.B }}` workflow-expression. GitHub's | |
| # own docs (Evaluate expressions in workflows and actions) do not | |
| # give an unambiguous answer for whether `||` returns an operand | |
| # VALUE or a coerced boolean when used outside an `if:` conditional — | |
| # community examples show both a value-returning ternary idiom | |
| # (`cond && 'a' || 'b'`) and a boolean-coercion note ("falsy values | |
| # are coerced to false / truthy to true") without reconciling the | |
| # two for the plain `secrets.A || secrets.B` (no `&&`) case used | |
| # here. Getting this wrong on a deploy-credential line would silently | |
| # pass the literal string "true"/"false" as the token instead of | |
| # failing loudly, so this step uses the unambiguous POSIX shell form | |
| # instead. | |
| - name: Trigger Railway redeploy | |
| if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' | |
| env: | |
| # Preferred (mt#3251): a Railway PROJECT token scoped to | |
| # minsky-reviewer / production. Not yet set as a repo secret — | |
| # resolves to empty string until the operator provisions it. | |
| RAILWAY_REVIEWER_TOKEN: ${{ secrets.RAILWAY_REVIEWER_TOKEN }} | |
| # Fallback: the generic repo secret. Wrong scope for THIS | |
| # service (mt#3251's root cause) but kept so the step degrades to | |
| # today's known failure mode instead of a new "secret not set" | |
| # one. Named distinctly from RAILWAY_TOKEN itself so the shell | |
| # below can tell "preferred token present" apart from "only the | |
| # fallback is present" without re-reading the secrets context. | |
| RAILWAY_TOKEN_FALLBACK: ${{ secrets.RAILWAY_TOKEN }} | |
| run: | | |
| # `-e` included deliberately (PR #2343 R1): an `npm install` failure | |
| # must abort rather than be misattributed to a later command. The | |
| # command whose own exit code carries diagnostic meaning | |
| # (`redeploy`) is wrapped in an `if` condition, which is exempt | |
| # from `-e`, so its tailored error message still prints. | |
| set -euo pipefail | |
| # DEPLOY_TOKEN_SOURCE names the secret ACTUALLY in use, mirroring the | |
| # variable of the same name in deploy-minsky-mcp.yml (PR #2952 R1). | |
| # The probe below clears a credential by name, and naming the wrong | |
| # one on the fallback path would be this task's own defect — a | |
| # message that sends the reader at a secret that is not involved. | |
| if [ -n "${RAILWAY_REVIEWER_TOKEN:-}" ]; then | |
| echo "Using RAILWAY_REVIEWER_TOKEN (per-project token, mt#3251)." | |
| RAILWAY_TOKEN="${RAILWAY_REVIEWER_TOKEN}" | |
| DEPLOY_TOKEN_SOURCE="RAILWAY_REVIEWER_TOKEN" | |
| else | |
| echo "::warning::RAILWAY_REVIEWER_TOKEN is not set — falling back to the generic RAILWAY_TOKEN secret, which is NOT scoped to the minsky-reviewer project and is expected to fail. See services/reviewer/DEPLOY.md for the operator action that fixes this (mt#3251)." | |
| RAILWAY_TOKEN="${RAILWAY_TOKEN_FALLBACK:-}" | |
| DEPLOY_TOKEN_SOURCE="RAILWAY_TOKEN" | |
| fi | |
| export RAILWAY_TOKEN | |
| if [ -z "${RAILWAY_TOKEN:-}" ]; then | |
| echo "::error::Neither RAILWAY_REVIEWER_TOKEN nor RAILWAY_TOKEN secrets are set — reviewer redeploy cannot be triggered." | |
| exit 1 | |
| fi | |
| npm install -g @railway/cli | |
| # Same probe as deploy-minsky-mcp.yml (mt#4075), and this file needs it | |
| # MORE: that one retries the install three times, this one runs it once. | |
| # npm's exit code reports on the package, not on the binary — the | |
| # package's postinstall downloads the executable from GitHub releases | |
| # and leaves a non-runnable stub when that download fails, which is | |
| # what happened on run 31645082761 (2026-08-12) in the sibling | |
| # workflow. Downstream, `railway whoami` and `railway redeploy` would | |
| # both fail here and the error text points at the TOKEN's scope, so a | |
| # failed install would read as a credential problem. | |
| ./scripts/ci/verify-railway-cli.sh "${DEPLOY_TOKEN_SOURCE}" | |
| # Railway identifier from services/reviewer/deploy.config.ts (kept | |
| # inline, matching the pre-existing convention in this file and in | |
| # deploy-minsky-mcp.yml). No `railway link` is needed: a | |
| # project-scoped RAILWAY_TOKEN carries its own project+environment | |
| # context (mt#3251) — the same reason cockpit-preview.yml needs | |
| # none. | |
| SERVICE_ID="3913e8a4-81ab-465a-aad8-b76b5e3f66ed" | |
| # Diagnostic only (non-fatal): prints the authenticated identity, | |
| # never the token. If RAILWAY_TOKEN is still the wrong-scope | |
| # fallback, this is where that surfaces, making a failure | |
| # self-diagnosing instead of another round of guessing. | |
| # | |
| # The email is masked because this repository's CI logs are public | |
| # (PR #2343 R1 non-blocking finding). | |
| echo "Railway auth identity:" | |
| railway whoami 2>&1 | sed -E 's/([A-Za-z0-9._%+-]+)@/***@/g' \ | |
| || echo "note: railway whoami did not report an authenticated identity (non-fatal diagnostic) — see the RAILWAY_TOKEN scope notes above if the redeploy call below also fails." | |
| if railway redeploy --from-source --service "${SERVICE_ID}" -y; then | |
| echo "Railway redeploy triggered successfully via the CLI." | |
| else | |
| rc=$? | |
| echo "::error::railway redeploy failed (exit ${rc}). See the 'whoami' output above to distinguish a token-scope problem from a genuine redeploy failure." | |
| exit "$rc" | |
| fi |