Skip to content
Merged
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
54 changes: 3 additions & 51 deletions .github/workflows/review_app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ jobs:
set +e
output=$(docker manifest inspect ghcr.io/hicommonwealth/commonwealth-ephemeral:${{ steps.set_effective_sha.outputs.sha }} 2>&1)
status=$?
echo "Manifest inspect output for commonwealth base:"
echo "$output"
echo "Exit code: $status"
if [ $status -eq 0 ]; then
echo "exists=true" >> $GITHUB_OUTPUT
else
Expand Down Expand Up @@ -144,69 +141,24 @@ jobs:
api_key: ${{ secrets.NEON_API_KEY }}
username: 'neondb_owner'
database: 'commonwealth'
suspend_timeout: 300 # scale to 0 compute after 5 minutes
ssl: require # DO NOT CHANGE THIS - we must use SSL since we are branching off prod
suspend_timeout: 300
ssl: require

- name: Migrate DB
id: migrate_db
env:
NODE_ENV: production
DATABASE_URL: ${{ steps.create_neon_branch.outputs.db_url }}
run: pnpm migrate-db

- name: Create Railway environment and deploy
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
RAILWAY_PARENT_ENV_ID: ${{ secrets.RAILWAY_PARENT_ENV_ID }}
run: |
pnpm -F railway deploy-review-app \
--env="pr-${{ steps.set_pr_number.outputs.pr_number }}" \
--commit="${{ steps.set_effective_sha.outputs.sha }}" \
--db-url="${{ steps.create_neon_branch.outputs.db_url }}"

- name: Comment on PR with Deployment URL
if: |
(github.event.issue.pull_request && github.event.comment.body == '/deploy') ||
github.event_name == 'workflow_dispatch'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
if (process.env.DEPLOYMENT_URL) {
const message = `🚀 Review app deployed!\n\nYou can access the review app at: [${process.env.DEPLOYMENT_URL}](https://${process.env.DEPLOYMENT_URL})`;

github.rest.issues.createComment({
issue_number: ${{ steps.set_pr_number.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: message
});
} else {
console.log('No deployment URL found to comment on PR');
const message = `⚠️ Review app deployed but deployment URL not found.\n\nCheck the Railway dashboard manually to get the URL.`
github.rest.issues.createComment({
issue_number: ${{ steps.set_pr_number.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: message
});
}

- name: Comment on PR if Deployment Fails
if: failure()
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const runUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
const message = `❌ Review app deployment failed!\n\nYou can view the failed workflow run for details: [View Run](${runUrl})`;
github.rest.issues.createComment({
issue_number: ${{ steps.set_pr_number.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: message
});
--db-url="${{ steps.create_neon_branch.outputs.db_url }}"

# TODO: check if this posts a new comment for each commit to an open PR if this updates the comment
# TODO: if this creates a new comment for each commit, we need to update if so it only posts a
Expand Down
124 changes: 79 additions & 45 deletions libs/railway/src/utils/awaitDeployment.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,103 @@
import { DeploymentStatus } from '../generated/graphql';
import { sdk } from '../sdk';

// Poll for deployment status every 60 seconds (avg deployment takes 45 seconds)
const STATUS_POLL_INTERVAL = 1_000 * 60;
// Wait for max 3 minutes for deployment to complete (3 retries)
const STATUS_MAX_WAIT_TIME = 1_000 * 60 * 3;

export async function getDeploymentStatus(deploymentId: string) {
const res = await sdk.deployment({
id: deploymentId,
});
const STATUS_POLL_INTERVAL_MS = 60_000; // 1 minute
const STATUS_MAX_WAIT_TIME_MS = 20 * 60_000; // 20 minutes

const TERMINAL_FAILURE_STATUSES = new Set<DeploymentStatus>([
DeploymentStatus.Crashed,
DeploymentStatus.Failed,
DeploymentStatus.Removed,
DeploymentStatus.Removing,
DeploymentStatus.NeedsApproval,
DeploymentStatus.Skipped,
]);

const SUCCESS_STATUSES = new Set<DeploymentStatus>([
DeploymentStatus.Success,
DeploymentStatus.Sleeping,
]);

const IN_PROGRESS_STATUSES = new Set<DeploymentStatus>([
DeploymentStatus.Queued,
DeploymentStatus.Waiting,
DeploymentStatus.Building,
DeploymentStatus.Initializing,
DeploymentStatus.Deploying,
]);

export type DeploymentSnapshot = {
status: DeploymentStatus;
url?: string | null;
serviceName: string;
};

export async function getDeploymentStatus(
deploymentId: string,
): Promise<DeploymentSnapshot> {
const res = await sdk.deployment({ id: deploymentId });

return {
status: res.deployment.status,
url: res.deployment.staticUrl,
serviceName: res.deployment.service.name,
};
}

export async function waitForDeploymentCompletion(deploymentId: string) {
export async function waitForDeploymentCompletion(
deploymentId: string,
): Promise<DeploymentSnapshot> {
const startTime = Date.now();

while (Date.now() - startTime < STATUS_MAX_WAIT_TIME) {
console.log(`Fetching status for deployment: ${deploymentId}`);
while (Date.now() - startTime < STATUS_MAX_WAIT_TIME_MS) {
const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);

console.log(
`[railway] [${elapsedSeconds}s] Fetching status for deployment ${deploymentId}`,
);

const dep = await getDeploymentStatus(deploymentId);
console.log(`Deployment '${deploymentId}' status: ${dep.status}`);

if (['SUCCESS', 'SLEEPING'].includes(dep.status)) {
console.log(`Deployment of '${dep.serviceName} succeeded!'`);
console.log(
`[railway] [${elapsedSeconds}s] service=${dep.serviceName} status=${dep.status}`,
);

if (SUCCESS_STATUSES.has(dep.status)) {
console.log(
`[railway] Deployment succeeded for service '${dep.serviceName}'`,
);
return dep;
} else if (
[
DeploymentStatus.Crashed,
DeploymentStatus.Failed,
DeploymentStatus.Removed,
DeploymentStatus.Removing,
DeploymentStatus.NeedsApproval,
DeploymentStatus.Skipped,
].includes(dep.status)
) {
}

if (TERMINAL_FAILURE_STATUSES.has(dep.status)) {
throw new Error(
`Deployment of '${dep.serviceName}' failed with status: ${dep.status}`,
`Railway deployment failed for service '${dep.serviceName}' with status: ${dep.status}`,
);
}

if (
[
DeploymentStatus.Queued,
DeploymentStatus.Waiting,
DeploymentStatus.Building,
DeploymentStatus.Initializing,
DeploymentStatus.Deploying,
].includes(dep.status)
) {
await new Promise((resolve) => {
console.log(
`Deployment not finished, retrying in ${STATUS_POLL_INTERVAL}`,
);
return setTimeout(resolve, STATUS_POLL_INTERVAL);
});
} else {
throw new Error(
`Unknown status '${dep.status}' for service ${dep.serviceName}`,
if (IN_PROGRESS_STATUSES.has(dep.status)) {
console.log(
`[railway] Deployment still in progress (${dep.status}); retrying in ${
STATUS_POLL_INTERVAL_MS / 1000
}s`,
);

await sleep(STATUS_POLL_INTERVAL_MS);
continue;
}

throw new Error(
`Unknown Railway deployment status '${dep.status}' for service '${dep.serviceName}'`,
);
}

throw new Error(`Failed to await deployment status`);
throw new Error(
`Timed out waiting for Railway deployment after ${
STATUS_MAX_WAIT_TIME_MS / 60000
} minutes. Check the Railway dashboard for final status.`,
);
}

function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useAuthModalStore } from '../../ui/modals';
import { updateCommunityThreadCount } from '../communities/getCommuityById';
import { removeThreadFromAllCaches } from './helpers/cache';

// tmep
export const buildDeleteThreadInput = async (
address: string,
thread: Thread,
Expand Down
Loading