[5650] chore(ci): cap Railway preview lifetime at 6h and sweep hourly - #5699
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe 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. ChangesRailway cleanup
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
.github/workflows/45-railway-cleanup.yml
Railway Preview Environment
Updated at 2026-08-03T18:21:50.329Z |
There was a problem hiding this comment.
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 winSerialize 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-staleconcurrency 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
📒 Files selected for processing (1)
.github/workflows/45-railway-cleanup.yml
| # 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" |
There was a problem hiding this comment.
🔒 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
fiRepository: 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
BASHPROBERepository: 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.
| case "$MAX_AGE_HOURS" in | ||
| '' | *[!0-9]*) echo "max_age_hours must be a whole number of hours" >&2; exit 1 ;; | ||
| esac |
There was a problem hiding this comment.
🗄️ 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
doneRepository: 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())
PYRepository: 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.
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
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.productionandpr-templateare 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