Skip to content

Commit 34d5606

Browse files
committed
Added GitHub Actions keepalive for Streamlit Cloud
1 parent 3098e64 commit 34d5606

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

.github/workflows/keepalive.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Keep the deployed Streamlit Community Cloud app from going to sleep.
2+
#
3+
# Streamlit Cloud puts free apps to sleep after inactivity, and the wrapper
4+
# page at https://<app>.streamlit.app/ always returns 200 even when the
5+
# Python container is asleep — so plain HTTP pings (UptimeRobot, Pingdom,
6+
# etc.) don't actually keep it awake. The reliable trick is to open a real
7+
# browser session that establishes a WebSocket to the backend, which is
8+
# what this workflow does via headless Chromium + Playwright.
9+
#
10+
# Schedule: every 6 hours. Each run takes ~3 min, so monthly cost is well
11+
# inside GitHub Actions' free allowance (unlimited for public repos, 2,000
12+
# min/month for private). Adjust the cron if you want a different cadence.
13+
14+
name: Keep Streamlit awake
15+
16+
on:
17+
schedule:
18+
- cron: "0 */6 * * *" # every 6 hours, on the hour
19+
workflow_dispatch: {} # manual run button in the Actions UI
20+
21+
jobs:
22+
ping:
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 5
25+
env:
26+
APP_URL: https://earth-time-machine.streamlit.app
27+
steps:
28+
- name: Set up Node
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: "20"
32+
33+
- name: Install Playwright + Chromium
34+
run: |
35+
npm init -y >/dev/null
36+
npm install --silent playwright
37+
npx --yes playwright install --with-deps chromium
38+
39+
- name: Open the app in a real browser
40+
run: |
41+
node -e "
42+
const { chromium } = require('playwright');
43+
(async () => {
44+
const url = process.env.APP_URL;
45+
console.log('Opening', url);
46+
const browser = await chromium.launch();
47+
const ctx = await browser.newContext();
48+
const page = await ctx.newPage();
49+
50+
// Load the app and wait for the WebSocket-driven UI to settle.
51+
// 'networkidle' waits for the Streamlit websocket bursts to quiet
52+
// down, which is a good proxy for 'session is established'.
53+
await page.goto(url, { waitUntil: 'networkidle', timeout: 120000 });
54+
55+
// If Streamlit had put the app to sleep, the wrapper shows a
56+
// 'Yes, get this app back up!' button. Click it if present.
57+
try {
58+
const wakeBtn = page.getByRole('button', { name: /get this app back up/i });
59+
if (await wakeBtn.count()) {
60+
console.log('Sleeping app detected — clicking wake button.');
61+
await wakeBtn.first().click({ timeout: 10000 });
62+
// Wait for the wake-up to complete and the real app to render.
63+
await page.waitForLoadState('networkidle', { timeout: 180000 });
64+
}
65+
} catch (e) {
66+
console.log('Wake button check skipped:', e.message);
67+
}
68+
69+
// Hold the session open long enough for Streamlit's backend to
70+
// register a real visit. 60s is comfortably above their threshold.
71+
await page.waitForTimeout(60000);
72+
73+
console.log('Session held; app should now be awake.');
74+
await browser.close();
75+
})().catch(err => {
76+
console.error(err);
77+
process.exit(1);
78+
});
79+
"

0 commit comments

Comments
 (0)