Skip to content

[5650] chore(ci): cap Railway preview lifetime at 6h and sweep hourly - #5699

Merged
mmabrouk merged 2 commits into
release/v0.108.1from
ci/preview-ttl-6h
Aug 3, 2026
Merged

[5650] chore(ci): cap Railway preview lifetime at 6h and sweep hourly#5699
mmabrouk merged 2 commits into
release/v0.108.1from
ci/preview-ttl-6h

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member

Symptom

Preview environments were living far longer than the stale-cleanup setting suggested, and Railway spend ran past the workspace usage limit.

The cleanup sweep ran once a day at 06:00 UTC with a 24 hour max age. The sweep frequency, not the age number, was the real ceiling: a preview created at 07:00 UTC survived until the next morning's sweep, about 23 hours, no matter what the age limit said.

Change

Before After
Sweep schedule daily, 06:00 UTC hourly
Max age 24h 6h

Together these cap a preview's real life at roughly 6 to 7 hours.

Side effect worth knowing

Age is measured from environment creation, not from last use. A preview on a long-lived pull request now disappears about 6 hours after it was created, even if someone is still reviewing on it. Getting it back means pushing a commit or re-running "14 - check PR preview". That is the trade we are making for lower spend.

Scope

This only affects pr-<number> environments inside the template project. production and pr-template are protected by name and are never swept.

A sweep with nothing to delete costs about three Railway API calls, so running it hourly is cheap against the 1000 requests per hour account limit.

Related: #5650

@dosubot dosubot Bot added size:XS This PR changes 0-9 lines, ignoring generated files. ci/cd labels Aug 3, 2026
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 3, 2026 5:42pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Chores
    • Railway cleanup now runs hourly instead of daily.
    • Preview environments are considered stale after 6 hours by default, enabling faster cleanup.
    • Improved cleanup reliability by validating cleanup settings before processing, helping ensure stale preview environments are handled consistently.

Walkthrough

The Railway cleanup workflow now runs hourly. Its default stale-preview age is six hours. Cleanup inputs pass through environment variables, and clone cleanup validates the age before script execution.

Changes

Railway cleanup

Layer / File(s) Summary
Cleanup schedule and stale threshold
.github/workflows/45-railway-cleanup.yml
The schedule changed from daily to hourly. The default stale-preview age changed from 24 to 6 hours.
Cleanup input validation and reporting
.github/workflows/45-railway-cleanup.yml
Cleanup steps pass values through environment variables. Clone cleanup rejects empty or non-numeric ages before invoking the cleanup script. Summary steps preserve their displayed fields.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the hourly sweep and six-hour Railway preview lifetime change.
Description check ✅ Passed The description directly explains the cleanup problem, schedule and age changes, scope, protections, and operational trade-offs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/preview-ttl-6h

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4318478b-13d1-42b8-b5de-64b9594564a7

📥 Commits

Reviewing files that changed from the base of the PR and between 69e2165 and 0ce2609.

📒 Files selected for processing (1)
  • .github/workflows/45-railway-cleanup.yml

Comment thread .github/workflows/45-railway-cleanup.yml
Comment thread .github/workflows/45-railway-cleanup.yml Outdated
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-08-03T18:21:50.329Z

@mmabrouk
mmabrouk changed the base branch from main to release/v0.108.1 August 3, 2026 17:41

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/45-railway-cleanup.yml (1)

7-20: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Serialize hourly stale cleanup runs.

This hourly schedule has no workflow/job concurrency group, so overlapping runs can call Railway delete APIs concurrently. Add a cleanup-stale concurrency group, or make the stale sweep safe for concurrent execution.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 737bb9b6-7361-4ebd-8d1d-439d0a7d707a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ce2609 and 63d7403.

📒 Files selected for processing (1)
  • .github/workflows/45-railway-cleanup.yml

Comment on lines +161 to +171
# Dispatch inputs reach the shell as env values, never as ${{ }} inside
# `run:`: template expansion happens before the shell parses the script,
# so an interpolated input can inject commands onto the runner.
MAX_AGE_HOURS: ${{ inputs.max_age_hours || '6' }}
run: |
case "$MAX_AGE_HOURS" in
'' | *[!0-9]*) echo "max_age_hours must be a whole number of hours" >&2; exit 1 ;;
esac
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' }}"
--stale-hours "$MAX_AGE_HOURS"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 \
  'Clean up stale previews|Clean up stale clone preview environments|RAILWAY_PREVIEW_MAX_AGE_HOURS|MAX_AGE_HOURS|MAX_AGE_SECONDS' \
  .github/workflows/45-railway-cleanup.yml \
  hosting/railway/oss/scripts/preview-cleanup-stale.sh \
  hosting/railway/oss/scripts/preview-clone-destroy.sh

marker="$(mktemp)"
trap 'rm -f "$marker"' EXIT

payload='$(printf injected > "$MARKER")'
MARKER="$marker" bash -c \
  'value="$1"; : "$((value * 3600))"' \
  _ "$payload" || true

if [ -s "$marker" ]; then
  echo "Unsafe command substitution observed"
  exit 1
fi

Repository: Agenta-AI/agenta

Length of output: 9257


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== workflow relevant steps =="
sed -n '120,185p' .github/workflows/45-railway-cleanup.yml

echo
echo "== stale cleanup relevant script =="
sed -n '1,95p' hosting/railway/oss/scripts/preview-cleanup-stale.sh

echo
echo "== clone destroy relevant script =="
sed -n '1,140p' hosting/railway/oss/scripts/preview-clone-destroy.sh

echo
echo "== behavioral probes =="

tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

cat > "$tmp" <<'BASHPROBE'
set -euo pipefail

max_age='$(printf injected > "$MARKER")'
MARKER="$1"
: "$((max_age * 3600))"
BASHPROBE

bash "$tmp" "$tmp" || true

if [ -s "$tmp" ]; then
  echo "$(basename "$tmp") file:"
  cat "$tmp"
  echo "command substitution observed before arithmetic"
else
  echo "no output file from arithmetic expression"
fi
BASHPROBE

Repository: Agenta-AI/agenta

Length of output: 12196


Validate max_age_hours before both cleanup steps.

Clean up stale previews passes raw inputs.max_age_hours into RAILWAY_PREVIEW_MAX_AGE_HOURS, and preview-cleanup-stale.sh evaluates it with $((MAX_AGE_HOURS * 3600)) before any validation. An invalid value can change the cleanup threshold or execute commands on the runner. Move validation before both steps and reuse the validated value.

Comment on lines +166 to +168
case "$MAX_AGE_HOURS" in
'' | *[!0-9]*) echo "max_age_hours must be a whole number of hours" >&2; exit 1 ;;
esac

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n 'STALE_HOURS \* 3600' \
  hosting/railway/oss/scripts/preview-clone-destroy.sh

for value in 6 08 9999999999999999999; do
  printf 'value=%s -> ' "$value"
  bash -c \
    'v="$1"; printf "%s\n" "$((v * 3600))"' \
    _ "$value" 2>&1 || true
done

Repository: Agenta-AI/agenta

Length of output: 343


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== workflow validation around MAX_AGE_HOURS =="
sed -n '130,195p' .github/workflows/45-railway-cleanup.yml

echo
echo "== script usage of max_age_seconds =="
sed -n '95,125p' hosting/railway/oss/scripts/preview-clone-destroy.sh

echo
echo "== surrounding bash arithmetic edge cases =="
python3 - <<'PY'
import re, subprocess, sys
sys.path.insert(0, str(subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()))
values = ["6", "08", "042", "9999999999999999999", "-1", "0", "9", "24", "3600", "3601"]
for value in values:
    cmd = ['bash','-c', f'v="{value}"; printf "value=%s expr=3600*$v seconds=$((3600*$v)) threshold=$((seconds / 3600 + 1))\\n" "$v"']
    out = subprocess.run(cmd, text=True, capture_output=True)
    if out.returncode != 0 or out.stderr:
        print("ERR", value, "ret", out.returncode, "stderr=", out.stderr.strip())
    else:
        print(out.stdout.strip())
PY

Repository: Agenta-AI/agenta

Length of output: 4420


Restrict MAX_AGE_HOURS before calling preview-clone-destroy.sh.

The current check allows unbounded decimal values, including leading zeros that Bash interprets as octal and values large enough to overflow in STALE_HOURS * 3600. Use an explicit positive decimal range before launching the script, or the sweep can fail or compute an unsafe deletion threshold.

@mmabrouk
mmabrouk merged commit 83f1cfd into release/v0.108.1 Aug 3, 2026
46 of 47 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant