diff --git a/.github/workflows/review_app.yml b/.github/workflows/review_app.yml index ec33e53c9ac..ba51f862f72 100644 --- a/.github/workflows/review_app.yml +++ b/.github/workflows/review_app.yml @@ -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 @@ -144,11 +141,10 @@ 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 }} @@ -156,57 +152,13 @@ jobs: - 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 diff --git a/libs/railway/src/utils/awaitDeployment.ts b/libs/railway/src/utils/awaitDeployment.ts index ab128734631..6c54ac215a1 100644 --- a/libs/railway/src/utils/awaitDeployment.ts +++ b/libs/railway/src/utils/awaitDeployment.ts @@ -1,15 +1,42 @@ 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.Crashed, + DeploymentStatus.Failed, + DeploymentStatus.Removed, + DeploymentStatus.Removing, + DeploymentStatus.NeedsApproval, + DeploymentStatus.Skipped, +]); + +const SUCCESS_STATUSES = new Set([ + DeploymentStatus.Success, + DeploymentStatus.Sleeping, +]); + +const IN_PROGRESS_STATUSES = new Set([ + 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 { + const res = await sdk.deployment({ id: deploymentId }); + return { status: res.deployment.status, url: res.deployment.staticUrl, @@ -17,53 +44,60 @@ export async function getDeploymentStatus(deploymentId: string) { }; } -export async function waitForDeploymentCompletion(deploymentId: string) { +export async function waitForDeploymentCompletion( + deploymentId: string, +): Promise { 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 { + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/packages/commonwealth/client/scripts/state/api/threads/deleteThread.ts b/packages/commonwealth/client/scripts/state/api/threads/deleteThread.ts index 3b28a004280..7de3951512d 100644 --- a/packages/commonwealth/client/scripts/state/api/threads/deleteThread.ts +++ b/packages/commonwealth/client/scripts/state/api/threads/deleteThread.ts @@ -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,