Skip to content
Draft
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
188 changes: 188 additions & 0 deletions .railway/railway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Railway Infrastructure as Code - Outpost services
// https://docs.railway.com/infrastructure-as-code
//
// Takes over from the per-service railway.toml files (Config as Code), which
// Railway stops reading on 2026-12-01 (same page). Unlike Config as Code, this
// file is NOT read at deploy time: changes reach Railway only through
// `railway config apply`, so editing a setting here and merging does nothing
// until someone applies it. Needs Railway CLI 5.42.1 or newer.
//
// `partial` limits what this file owns to the services declared below. A
// whole-project file deletes every resource it omits on apply, and this file
// does not declare the Postgres databases, so without it an apply would plan to
// delete them.
//
// Cutover. The change that adds this file also deletes the railway.toml files,
// so a service still pointed at one has no build config until it is applied.
// 1. Before merging: `railway config pull` in each environment and copy its
// variables in as preserve(), along with its domains (custom and
// *.up.railway.app: the webhook services' URLs are registered with
// GitHub, Linear and Teams). Then add the environment to
// VARIABLES_IMPORTED.
// 2. When the change reaches an environment's deploy branch (`main` for
// production, `staging` for staging), in the same sitting:
// a. Clear every service's "Config file path" in the dashboard. IaC
// cannot manage a service that Config as Code still manages.
// b. `railway config plan`. It may change only the build and deploy
// settings this file sets. Stop if it removes or changes anything else:
// variables, domains, replicas, dashboard-only settings, the deploy
// trigger of slack-bot, teams-bot or linear-sync, or (in staging) an
// outpost-release-bot service.
// c. `railway config apply`.
//
// Build context is the repo ROOT for every service (the Dockerfiles run
// `turbo prune`), so no service sets a root directory.

import { defineRailway, github, service } from 'railway/iac';

export const partial = 'outpost-services';

// Environments whose variables from `railway config pull` are in this file.
// Each one is listed separately because staging and production hold different
// values: importing staging's does nothing to protect production's.
const VARIABLES_IMPORTED: Environment[] = [];

// Each environment deploys from its own branch: staging from `staging`,
// production from `main`. Anything else is an error rather than a silent
// fallback to `main`.
const BRANCHES = {
production: 'main',
staging: 'staging',
} as const;

type Environment = keyof typeof BRANCHES;

const isEnvironment = (name: string): name is Environment => Object.hasOwn(BRANCHES, name);

export default defineRailway((ctx, project) => {
const environment = ctx.environmentName ?? ctx.environment;
if (!environment) {
throw new Error(
'No Railway environment selected. Link one with `railway environment` first.',
);
}
if (!isEnvironment(environment)) {
throw new Error(
`No branch mapped for Railway environment "${environment}". ` +
`Known environments: ${Object.keys(BRANCHES).join(', ')}.`,
);
}
const branch = BRANCHES[environment];

// Railway does not document which command it passes here, so a run without
// one is treated as read-only. That
// makes this guard catch an apply only when the CLI says `apply`; the plan
// check in cutover step 2b stays the real safety net.
const readOnly = ctx.command === undefined || ctx.command === 'plan' || ctx.command === 'pull';
if (!VARIABLES_IMPORTED.includes(environment) && !readOnly) {
throw new Error(
`Variables for "${environment}" are not declared in .railway/railway.ts yet, so an apply would delete them. ` +
'Import them with `railway config pull` first (see the cutover steps at the top of the file).',
);
}

// checkSuites turns on "wait for CI": a push deploys only after the CI check
// suite passes on that commit, as both environments do today.
const deployed = github('CopilotKit/outpost', { branch, checkSuites: true });

// slack-bot, teams-bot and linear-sync are optional integrations, deployed
// by hand and left offline until their credentials are configured, so this
// file does not set a branch for them. github() defaults the branch to
// `main`, which would add a deploy trigger, so it is passed as null; the SDK
// then drops the key, leaving the branch out of the plan. Cutover step 2b
// checks that Railway reads that as "no change".
const manual = github('CopilotKit/outpost', { branch: null });

const dockerfile = (app: string) => ({
builder: 'DOCKERFILE' as const,
dockerfilePath: `apps/${app}/Dockerfile`,
});

// web's healthcheck was set in the dashboard, not in its railway.toml. It is
// declared here so it is version-controlled like the other services'. Its
// other dashboard-only settings are left out; cutover step 2b checks that
// the plan leaves them alone.
const web = service('outpost-web', {
source: deployed,
build: dockerfile('web'),
deploy: { healthcheckPath: '/api/health' },
});

const discordBot = service('outpost-discord-bot', {
source: deployed,
build: dockerfile('discord-bot'),
deploy: { restartPolicyType: 'ALWAYS' },
});

const githubApp = service('outpost-github-app', {
source: deployed,
build: dockerfile('github-app'),
deploy: { restartPolicyType: 'ALWAYS', healthcheckPath: '/health' },
});

const linearSync = service('outpost-linear-sync', {
source: manual,
build: dockerfile('linear-sync'),
deploy: { restartPolicyType: 'ALWAYS', healthcheckPath: '/health' },
});

const slackBot = service('outpost-slack-bot', {
source: manual,
build: dockerfile('slack-bot'),
deploy: { restartPolicyType: 'ALWAYS' },
});

const teamsBot = service('outpost-teams-bot', {
source: manual,
build: dockerfile('teams-bot'),
deploy: { restartPolicyType: 'ALWAYS', healthcheckPath: '/health' },
});

const worker = service('outpost-worker', {
source: deployed,
build: dockerfile('worker'),
deploy: { restartPolicyType: 'ALWAYS', healthcheckPath: '/health' },
});

const resources = [web, discordBot, githubApp, linearSync, slackBot, teamsBot, worker];

// The release bot runs in production only: it posts to the community
// Discord channels, so a staging copy would announce the same releases twice.
if (environment === 'production') {
// A cron job, not a long-running process: it announces whatever is new
// and exits. restartPolicyType NEVER because a completed run exiting 0
// is success, not a crash; ALWAYS would restart it in a loop and run the
// job back to back.
//
// watchPatterns keeps unrelated pushes from redeploying it. Every entry
// is anchored with a leading slash: these are gitignore-style patterns,
// so an unanchored `package.json` would match every package.json in the
// repo. If the release bot ever depends on a workspace package, add
// `/packages/**` here, or changes to that package will not redeploy it.
const releaseBot = service('outpost-release-bot', {
source: deployed,
build: {
...dockerfile('release-bot'),
watchPatterns: [
'/apps/release-bot/**',
'!/apps/release-bot/README.md',
'!/apps/release-bot/.env.example',
'!/apps/release-bot/src/__tests__/**',
'!/apps/release-bot/vitest.config.ts',
'!/apps/release-bot/tsconfig.test.json',
'/package.json',
'/pnpm-lock.yaml',
'/pnpm-workspace.yaml',
'/.npmrc',
'/tsconfig.json',
'/turbo.json',
'/.dockerignore',
],
},
deploy: { restartPolicyType: 'NEVER', cronSchedule: '0 * * * *' },
});
resources.push(releaseBot);
}

return project('outpost', { resources });
});
10 changes: 0 additions & 10 deletions apps/discord-bot/railway.toml

This file was deleted.

2 changes: 1 addition & 1 deletion apps/docs/deployment/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ <h2>Platform-Specific Guides</h2>
<h3>Railway (Recommended)</h3>
<p>
Railway auto-deploys from GitHub and natively supports Docker-based services.
The repo includes a <code>railway.toml</code> config for all five services.
The repo includes a <code>.railway/railway.ts</code> config for all services, applied with <code>railway config apply</code>.
</p>
<ol>
<li>Fork or push the repo to GitHub</li>
Expand Down
11 changes: 0 additions & 11 deletions apps/github-app/railway.toml

This file was deleted.

11 changes: 0 additions & 11 deletions apps/linear-sync/railway.toml

This file was deleted.

16 changes: 8 additions & 8 deletions apps/release-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ summary written to replace them.

## Deployment

A Railway cron service. `railway.toml` carries the build config, the restart
A Railway cron service. Its entry in the repo's `.railway/railway.ts` carries the build config, the restart
policy, the schedule and the watch patterns, so a recreated service is still
scheduled rather than running once at deploy and never again, and an unrelated
push elsewhere in the monorepo does not trigger an extra run.
Expand Down Expand Up @@ -364,8 +364,8 @@ channel at once could both decide the same release is unannounced and both post
it. It also means the interval is not a safety limit: Railway allows anything
down to 5 minutes, and hourly is a choice about cost and channel noise.

To change it, edit `cronSchedule` in `railway.toml` and redeploy. Railway only
applies the schedule from that file on a deploy.
To change it, edit `cronSchedule` in `.railway/railway.ts` and run
`railway config apply`. Railway does not read that file on a deploy.

### YouTube API key

Expand Down Expand Up @@ -397,11 +397,11 @@ allowance of 10,000 a day.

1. In the Outpost Railway project, add a service from the `CopilotKit/outpost`
repo, named `outpost-release-bot`.
2. Under Settings -> Config-as-code, set the config file path to
`apps/release-bot/railway.toml`. Leave Root Directory empty: the build context
has to be the repo root, because the Dockerfile runs `turbo prune` across the
monorepo. The file sets the Dockerfile build, the hourly schedule, the restart
policy and the watch patterns, so none of those need setting in the dashboard.
2. Leave Root Directory and the config file path empty: the build context has
to be the repo root, because the Dockerfile runs `turbo prune` across the
monorepo. Run `railway config apply`: `.railway/railway.ts` sets the
Dockerfile build, the hourly schedule, the restart policy and the watch
patterns, so none of those need setting in the dashboard.
3. Set the variables from the table above under Variables. At minimum
`DISCORD_BOT_TOKEN`, plus a channel id per source you want running. Release
sources also need `OPENAI_API_KEY` and `GITHUB_TOKEN`.
Expand Down
81 changes: 0 additions & 81 deletions apps/release-bot/railway.toml

This file was deleted.

10 changes: 0 additions & 10 deletions apps/slack-bot/railway.toml

This file was deleted.

11 changes: 0 additions & 11 deletions apps/teams-bot/railway.toml

This file was deleted.

11 changes: 0 additions & 11 deletions apps/worker/railway.toml

This file was deleted.

Loading
Loading