Skip to content
Closed
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
75 changes: 75 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Deploy to Fly.io

# Deploy on every push to master, excluding docs/assets/blog/markdown-only changes,
# plus manual runs from the Actions tab.
on:
push:
branches: [master]
paths-ignore:
- 'docs/**'
- 'assets/**'
- 'blog/**'
- '**.md'
workflow_dispatch:

permissions:
contents: read

# Prevent overlapping deploys from racing each other (a queued deploy waits;
# we do NOT cancel in-flight ones because a partial Fly deploy is undesirable).
concurrency:
group: fly-deploy
cancel-in-progress: false

jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up flyctl
# Pinned to the v1 major tag (not @master) to avoid supply-chain drift
# of an action that runs with FLY_API_TOKEN in scope.
uses: superfly/flyctl-actions/setup-flyctl@v1

# Single step: stage the secret env values into the Fly app, then deploy.
# `--stage` queues the secret update so it is applied as part of the very
# next deploy (one machine restart instead of two, and the values ship
# WITH this deploy rather than as a separate, racing deploy).
#
# SECRET HYGIENE: every secret — including FLY_API_TOKEN — is passed only via
# this step's `env:` block. The run script references them as shell vars ($VAR)
# and NEVER interpolates ${{ secrets.X }} directly on a command line, which
# would risk leaking values into logs and breaks on values with special chars.
# No echo / set -x / `| cat` of any secret.
#
# RESERVED PREFIX: GitHub reserves the GITHUB_ prefix for env vars and
# auto-injects its own GITHUB_TOKEN, so we must NOT name our env vars
# GITHUB_TOKEN / GITHUB_USERNAME (they would be ignored/overridden). We keep
# them under GH_TOKEN / GH_USERNAME here and map them to the env var names
# the app actually reads (GITHUB_TOKEN / GITHUB_USERNAME) inside the command.
- name: Stage secrets and deploy
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GH_USERNAME: ${{ secrets.GH_USERNAME }}
GOOGLE_API_KEYS: ${{ secrets.GOOGLE_API_KEYS }}
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_PARENT_PAGE_ID: ${{ secrets.NOTION_PARENT_PAGE_ID }}
DEVTO_API_KEY: ${{ secrets.DEVTO_API_KEY }}
DASHBOARD_PASSWORD_HASH: ${{ secrets.DASHBOARD_PASSWORD_HASH }}
IMAGE_PUBLIC_BASE_URL: ${{ secrets.IMAGE_PUBLIC_BASE_URL }}
run: |
flyctl secrets set --stage \
GITHUB_TOKEN="$GH_TOKEN" \
GITHUB_USERNAME="$GH_USERNAME" \
GOOGLE_API_KEYS="$GOOGLE_API_KEYS" \
NOTION_TOKEN="$NOTION_TOKEN" \
NOTION_PARENT_PAGE_ID="$NOTION_PARENT_PAGE_ID" \
DEVTO_API_KEY="$DEVTO_API_KEY" \
DASHBOARD_PASSWORD_HASH="$DASHBOARD_PASSWORD_HASH" \
IMAGE_PUBLIC_BASE_URL="$IMAGE_PUBLIC_BASE_URL"
flyctl deploy --remote-only
61 changes: 61 additions & 0 deletions fly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# fly.toml — DevNotion dashboard (server-rendered Express app run via tsx)
#
# Deploy: flyctl deploy --remote-only
# Secrets are NOT defined here — they are pushed from GitHub Actions via
# `flyctl secrets set --stage` (see .github/workflows/deploy.yml).

app = "devnotion-dashboard"
primary_region = "iad"

[build]
dockerfile = "Dockerfile"

# Non-secret runtime config. Secrets (tokens/keys/hashes) are injected separately
# as Fly secrets so they never live in source control.
[env]
NODE_ENV = "production"
PUBLISH_TARGETS = "notion,devto"
# The Express server reads $PORT first (falls back to DASHBOARD_PORT=3000).
PORT = "3000"
# Persist run history on the mounted volume (see [mounts] below).
# If you do NOT attach a volume, comment this out — the app will fall back to
# an ephemeral .devnotion-runs.json that is lost on every machine restart.
DEVNOTION_RUNS_PATH = "/data/.devnotion-runs.json"

[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = false
auto_start_machines = true
# In-memory sessions + in-memory rate-limiter + (without a volume) the run-store
# live inside a single machine's process. Keep at least one machine always
# running so a logged-in session and the run history survive between requests.
# Trade-off: scaling to zero (min_machines_running = 0) would cold-start a fresh
# process, wiping all in-memory sessions (forcing re-login) and the rate-limiter
# state, and losing any run-store data not backed by a persistent volume.
min_machines_running = 1

[[http_service.checks]]
method = "GET"
path = "/health"
interval = "15s"
timeout = "5s"
grace_period = "30s"

# A "New Run" / weekly dispatch (LLM + GitHub + Notion/DEV.to + image render) can
# take several MINUTES. Generous machine sizing avoids OOM during resvg rendering.
# NOTE: `size` already fixes the CPU topology, so we only override `memory`.
# Do NOT also set `cpus` here — it is redundant with `size` and can conflict.
[[vm]]
size = "shared-cpu-1x"
memory = "1gb"

# Persistent volume so run history (DEVNOTION_RUNS_PATH) and generated PNGs survive
# machine restarts/redeploys. Create once:
# flyctl volumes create devnotion_data --region iad --size 1
# NOTE: a single machine can mount this volume; with min_machines_running = 1 that
# is exactly our topology. assets/generated should be symlinked/configured onto
# /data if you also want images to persist (see notes).
[mounts]
source = "devnotion_data"
destination = "/data"