Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions .github/workflows/14-check-pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,49 @@ concurrency:
cancel-in-progress: true

jobs:
# Resolve the preview mode ONCE per run so setup and deploy always agree,
# even if the RAILWAY_PREVIEW_MODE repo variable is flipped mid-run.
# 'legacy' = one Railway project per PR (the original path);
# 'clone' = one environment per PR, cloned from the template project
# (issue #5650; see hosting/railway/oss/README.md).
mode:
if: github.event_name == 'workflow_dispatch' || !github.event.pull_request.draft
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.resolve.outputs.mode }}
steps:
- name: Resolve RAILWAY_PREVIEW_MODE
id: resolve
env:
MODE: ${{ vars.RAILWAY_PREVIEW_MODE || 'legacy' }}
run: echo "mode=${MODE}" >> "$GITHUB_OUTPUT"

build:
if: github.event_name == 'workflow_dispatch' || !github.event.pull_request.draft
uses: ./.github/workflows/42-railway-build.yml
with:
pr_number: ${{ github.event.pull_request.number || inputs.pr_number }}

setup:
needs: [build]
if: needs.build.result == 'success'
needs: [build, mode]
if: needs.build.result == 'success' && needs.mode.result == 'success'
uses: ./.github/workflows/41-railway-setup.yml
with:
pr_number: ${{ needs.build.outputs.pr_number }}
image_tag: ${{ needs.build.outputs.image_tag }}
mode: ${{ needs.mode.outputs.mode }}
secrets: inherit

deploy:
needs: [build, setup]
needs: [build, setup, mode]
if: |
needs.build.result == 'success' &&
needs.setup.result == 'success'
uses: ./.github/workflows/43-railway-deploy.yml
with:
pr_number: ${{ needs.build.outputs.pr_number }}
image_tag: ${{ needs.build.outputs.image_tag }}
mode: ${{ needs.mode.outputs.mode }}
secrets: inherit

tests:
Expand Down
59 changes: 55 additions & 4 deletions .github/workflows/41-railway-setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ on:
required: false
type: string
default: ""
mode:
description: "Preview mode: 'legacy' (project per PR) or 'clone' (environment cloned from the template project; issue #5650)"
required: false
type: string
default: "legacy"
secrets:
RAILWAY_TOKEN:
required: true
Expand All @@ -33,6 +38,11 @@ on:
required: false
type: string
default: ""
mode:
description: "Preview mode: 'legacy' (project per PR) or 'clone' (environment cloned from the template project; issue #5650)"
required: false
type: string
default: "legacy"

permissions:
contents: read
Expand All @@ -48,12 +58,14 @@ jobs:
setup:
runs-on: ubuntu-latest
outputs:
project_name: ${{ steps.setup.outputs.project_name }}
environment_name: ${{ steps.setup.outputs.environment_name }}
# Exactly one of the two steps runs, selected by inputs.mode.
project_name: ${{ steps.setup.outputs.project_name || steps.clone.outputs.project_name }}
environment_name: ${{ steps.setup.outputs.environment_name || steps.clone.outputs.environment_name }}
steps:
- uses: actions/checkout@v6

- name: Install Railway CLI
if: inputs.mode != 'clone'
run: |
npm install -g @railway/cli@latest
railway --version
Expand All @@ -65,6 +77,7 @@ jobs:
fi

- name: Setup preview environment
if: inputs.mode != 'clone'
id: setup
env:
PR_NUMBER: ${{ inputs.pr_number }}
Expand Down Expand Up @@ -100,6 +113,43 @@ jobs:
exit "$setup_status"
fi

# Clone mode (issue #5650): create-or-update the per-PR environment in
# the template project, patch the app images to this run's tag, wait for
# green, and smoke it — all in one script, pure GraphQL (no Railway
# CLI). The deploy workflow (43) then only re-verifies and posts the
# preview URL.
- name: Create or update clone preview environment
if: inputs.mode == 'clone'
id: clone
env:
PR_NUMBER: ${{ inputs.pr_number }}
IMAGE_TAG: ${{ inputs.image_tag }}
run: |
chmod +x hosting/railway/oss/scripts/*.sh

# Same artifact contract as the legacy step: persist the full log so
# "Upload setup log" publishes it regardless of live-log truncation.
log_file="${GITHUB_WORKSPACE:-$PWD}/railway-setup-${PR_NUMBER:-unknown}.log"

set +e
hosting/railway/oss/scripts/preview-clone-create.sh 2>&1 | tee "$log_file"
setup_status=${PIPESTATUS[0]}
set -e

if [ "$setup_status" -ne 0 ]; then
{
echo "### Railway Preview Setup (clone mode) — Failed"
echo
echo "<details><summary>Setup log (last 100 lines)</summary>"
echo
echo '```'
tail -n 100 "$log_file" 2>/dev/null
echo '```'
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
exit "$setup_status"
fi

- name: Upload setup log
if: always()
# Diagnostics only: a failed/duplicate upload must never fail the job.
Expand All @@ -120,6 +170,7 @@ jobs:
echo "| Item | Value |"
echo "| --- | --- |"
echo "| PR | \`${{ inputs.pr_number }}\` |"
echo "| Project | \`${{ steps.setup.outputs.project_name }}\` |"
echo "| Environment | \`${{ steps.setup.outputs.environment_name }}\` |"
echo "| Mode | \`${{ inputs.mode || 'legacy' }}\` |"
echo "| Project | \`${{ steps.setup.outputs.project_name || steps.clone.outputs.project_name }}\` |"
echo "| Environment | \`${{ steps.setup.outputs.environment_name || steps.clone.outputs.environment_name }}\` |"
} >> "$GITHUB_STEP_SUMMARY"
78 changes: 68 additions & 10 deletions .github/workflows/43-railway-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ on:
description: "PR number"
required: true
type: string
mode:
description: "Preview mode: 'legacy' (project per PR) or 'clone' (environment cloned from the template project; issue #5650)"
required: false
type: string
default: "legacy"
secrets:
RAILWAY_TOKEN:
required: true
Expand Down Expand Up @@ -48,6 +53,11 @@ on:
description: "PR number"
required: true
type: string
mode:
description: "Preview mode: 'legacy' (project per PR) or 'clone' (environment cloned from the template project; issue #5650)"
required: false
type: string
default: "legacy"

permissions:
contents: read
Expand All @@ -64,14 +74,16 @@ jobs:
deploy:
runs-on: ubuntu-latest
outputs:
preview_url: ${{ steps.deploy.outputs.preview_url }}
project_name: ${{ steps.deploy.outputs.project_name }}
environment_name: ${{ steps.deploy.outputs.environment_name }}
railway_logs_url: ${{ steps.deploy.outputs.railway_logs_url }}
# Exactly one of the two steps runs, selected by inputs.mode.
preview_url: ${{ steps.deploy.outputs.preview_url || steps.verify.outputs.preview_url }}
project_name: ${{ steps.deploy.outputs.project_name || steps.verify.outputs.project_name }}
environment_name: ${{ steps.deploy.outputs.environment_name || steps.verify.outputs.environment_name }}
railway_logs_url: ${{ steps.deploy.outputs.railway_logs_url || steps.verify.outputs.railway_logs_url }}
steps:
- uses: actions/checkout@v6

- name: Install Railway CLI
if: inputs.mode != 'clone'
run: |
npm install -g @railway/cli@latest
railway --version
Expand All @@ -83,6 +95,7 @@ jobs:
fi

- name: Deploy preview environment
if: inputs.mode != 'clone'
id: deploy
env:
PR_NUMBER: ${{ inputs.pr_number }}
Expand Down Expand Up @@ -216,6 +229,51 @@ jobs:
exit 1
fi

# Clone mode (issue #5650): setup (41) already created the environment,
# patched the images and smoked it, so the legacy deploy is redundant.
# This step re-verifies the smoke endpoints (mutation-free) and emits
# the same outputs the legacy step would, keeping the 14-chain's job
# graph (tests + PR comment) intact.
- name: Verify clone preview environment
if: inputs.mode == 'clone'
id: verify
env:
PR_NUMBER: ${{ inputs.pr_number }}
IMAGE_TAG: ${{ inputs.image_tag }}
run: |
chmod +x hosting/railway/oss/scripts/*.sh

# Same artifact contract as the legacy step: persist the full log so
# "Upload deploy log" publishes it regardless of live-log truncation.
log_file="${GITHUB_WORKSPACE:-$PWD}/railway-deploy-${PR_NUMBER:-unknown}.log"

set +e
hosting/railway/oss/scripts/preview-clone-create.sh --verify-only 2>&1 | tee "$log_file"
verify_status=${PIPESTATUS[0]}
set -e

status_label="Deployed"
[ "$verify_status" -ne 0 ] && status_label="Failed"
{
echo "### Railway Preview Deploy (clone mode)"
echo
echo "| Item | Value |"
echo "| --- | --- |"
echo "| PR | \`${PR_NUMBER}\` |"
echo "| Image tag | \`${IMAGE_TAG}\` |"
echo "| Status | ${status_label} |"
if [ "$verify_status" -ne 0 ]; then
echo
echo "<details><summary>Verify log (last 100 lines)</summary>"
echo
echo '```'
tail -n 100 "$log_file" 2>/dev/null
echo '```'
echo "</details>"
fi
} >> "$GITHUB_STEP_SUMMARY"
exit "$verify_status"

- name: Upload deploy log
if: always()
# Diagnostics only: a failed/duplicate upload must never fail the job.
Expand All @@ -229,17 +287,17 @@ jobs:
retention-days: 7

- name: Post preview URL as PR comment
if: inputs.pr_number != '' && steps.deploy.outputs.preview_url != ''
if: inputs.pr_number != '' && (steps.deploy.outputs.preview_url != '' || steps.verify.outputs.preview_url != '')
uses: actions/github-script@v7
with:
script: |
const prNumber = parseInt('${{ inputs.pr_number }}');
const previewUrl = '${{ steps.deploy.outputs.preview_url }}';
const projectName = '${{ steps.deploy.outputs.project_name }}';
const previewUrl = '${{ steps.deploy.outputs.preview_url || steps.verify.outputs.preview_url }}';
const projectName = '${{ steps.deploy.outputs.project_name || steps.verify.outputs.project_name }}';
const imageTag = '${{ inputs.image_tag }}';
const marker = '<!-- railway-preview-bot -->';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const railwayLogsUrl = '${{ steps.deploy.outputs.railway_logs_url }}';
const railwayLogsUrl = '${{ steps.deploy.outputs.railway_logs_url || steps.verify.outputs.railway_logs_url }}';

const body = [
marker,
Expand Down Expand Up @@ -290,8 +348,8 @@ jobs:
const imageTag = '${{ inputs.image_tag }}';
const marker = '<!-- railway-preview-bot -->';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const previewUrl = '${{ steps.deploy.outputs.preview_url }}';
const railwayLogsUrl = '${{ steps.deploy.outputs.railway_logs_url }}';
const previewUrl = '${{ steps.deploy.outputs.preview_url || steps.verify.outputs.preview_url }}';
const railwayLogsUrl = '${{ steps.deploy.outputs.railway_logs_url || steps.verify.outputs.railway_logs_url }}';

const body = [
marker,
Expand Down
30 changes: 30 additions & 0 deletions .github/workflows/45-railway-cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ permissions:

env:
RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
# This workflow triggers directly (PR close / cron / dispatch), so it cannot
# receive the mode resolved by workflow 14; it reads the repo variable at
# its own run time. 'legacy' = project per PR; 'clone' = environment per PR
# in the template project (issue #5650).
RAILWAY_PREVIEW_MODE: ${{ vars.RAILWAY_PREVIEW_MODE || 'legacy' }}

jobs:
destroy-pr-preview:
Expand All @@ -49,12 +54,24 @@ jobs:
fi

- name: Destroy preview environment
if: env.RAILWAY_PREVIEW_MODE != 'clone'
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: |
chmod +x hosting/railway/oss/scripts/preview-destroy.sh
hosting/railway/oss/scripts/preview-destroy.sh

# Clone mode (issue #5650): the preview is an environment in the
# template project, not a project — delete just that environment
# (idempotent: absent = success).
- name: Destroy clone preview environment
if: env.RAILWAY_PREVIEW_MODE == 'clone'
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: |
chmod +x hosting/railway/oss/scripts/preview-clone-destroy.sh
hosting/railway/oss/scripts/preview-clone-destroy.sh

- name: Update preview comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
Expand Down Expand Up @@ -128,6 +145,19 @@ jobs:
chmod +x hosting/railway/oss/scripts/preview-cleanup-stale.sh
hosting/railway/oss/scripts/preview-cleanup-stale.sh

# Clone-mode equivalent of the stale sweep (issue #5650): delete pr-*
# preview environments older than the max age in the template project.
# Runs in BOTH modes on purpose — after a rollback to legacy, clone
# environments created before the flip would otherwise linger forever.
# A sweep with nothing to delete costs ~3 API calls.
- name: Clean up stale clone preview environments
env:
RAILWAY_PREVIEW_DRY_RUN: ${{ inputs.dry_run || 'false' }}
run: |
chmod +x hosting/railway/oss/scripts/preview-clone-destroy.sh
hosting/railway/oss/scripts/preview-clone-destroy.sh \
--stale-hours "${{ inputs.max_age_hours || '24' }}"

- name: Summary
if: always()
run: |
Expand Down
Loading
Loading