mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Fix flyctl cleanup; move/copy scripts to support preview deploys in grist-core
Summary: - Fly flyctl cleanup which has been failing; deleting volumes before app no longer seems needed - Move fly-deploy and add workflow scripts to make fly preview work in grist-core too Test Plan: Tested that previews still work in this repo; and tested with a copy in grist-core. Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D3928
This commit is contained in:
		
							parent
							
								
									3fa5125cf7
								
							
						
					
					
						commit
						331aa3152f
					
				
							
								
								
									
										23
									
								
								.github/workflows/fly-cleanup.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/fly-cleanup.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					name: Fly Cleanup
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  schedule:
 | 
				
			||||||
 | 
					    # Once a day, clean up jobs marked as expired
 | 
				
			||||||
 | 
					    - cron: '50 12 * * *'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Allows running this workflow manually from the Actions tab
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  clean:
 | 
				
			||||||
 | 
					      name: Clean stale deployed apps
 | 
				
			||||||
 | 
					      runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					      if: github.repository_owner == 'gristlabs'
 | 
				
			||||||
 | 
					      steps:
 | 
				
			||||||
 | 
					        - uses: actions/checkout@v3
 | 
				
			||||||
 | 
					        - uses: superfly/flyctl-actions/setup-flyctl@master
 | 
				
			||||||
 | 
					          with:
 | 
				
			||||||
 | 
					            version: 0.0.525
 | 
				
			||||||
 | 
					        - run: node buildtools/fly-deploy.js clean
 | 
				
			||||||
							
								
								
									
										64
									
								
								.github/workflows/fly.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								.github/workflows/fly.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					name: Fly Deploy
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [ main ]
 | 
				
			||||||
 | 
					    types: [labeled, unlabeled, closed, opened, synchronize, reopened]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Allows running this workflow manually from the Actions tab
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
 | 
				
			||||||
 | 
					  BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  deploy:
 | 
				
			||||||
 | 
					    name: Deploy app
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    # Deploy when the 'preview' label is added, or when PR is updated with this label present.
 | 
				
			||||||
 | 
					    if: |
 | 
				
			||||||
 | 
					          github.repository_owner == 'gristlabs' &&
 | 
				
			||||||
 | 
					          github.event_name == 'pull_request' && (
 | 
				
			||||||
 | 
					            github.event.action == 'labeled' ||
 | 
				
			||||||
 | 
					            github.event.action == 'opened' ||
 | 
				
			||||||
 | 
					            github.event.action == 'synchronize' ||
 | 
				
			||||||
 | 
					            github.event.action == 'reopened'
 | 
				
			||||||
 | 
					          ) &&
 | 
				
			||||||
 | 
					          contains(github.event.pull_request.labels.*.name, 'preview')
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
 | 
					      - uses: superfly/flyctl-actions/setup-flyctl@master
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          version: 0.0.525
 | 
				
			||||||
 | 
					      - id: fly_deploy
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          node buildtools/fly-deploy.js deploy
 | 
				
			||||||
 | 
					          flyctl config -c ./fly.toml env | awk '/APP_HOME_URL/{print "DEPLOY_URL=" $2}' >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					          flyctl config -c ./fly.toml env | awk '/FLY_DEPLOY_EXPIRATION/{print "EXPIRES=" $2}' >> $GITHUB_OUTPUT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - uses: actions/github-script@v6
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          script: |
 | 
				
			||||||
 | 
					            github.rest.issues.createComment({
 | 
				
			||||||
 | 
					              issue_number: context.issue.number,
 | 
				
			||||||
 | 
					              owner: context.repo.owner,
 | 
				
			||||||
 | 
					              repo: context.repo.repo,
 | 
				
			||||||
 | 
					              body: `Deployed as ${{ steps.fly_deploy.outputs.DEPLOY_URL }} (until ${{ steps.fly_deploy.outputs.EXPIRES }})`
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  destroy:
 | 
				
			||||||
 | 
					    name: Remove app
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    # Remove the deployment when 'preview' label is removed, or the PR is closed.
 | 
				
			||||||
 | 
					    if: |
 | 
				
			||||||
 | 
					          github.repository_owner == 'gristlabs' &&
 | 
				
			||||||
 | 
					          github.event_name == 'pull_request' &&
 | 
				
			||||||
 | 
					          (github.event.action == 'closed' ||
 | 
				
			||||||
 | 
					           (github.event.action == 'unlabeled' && github.event.label.name == 'preview'))
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v3
 | 
				
			||||||
 | 
					      - uses: superfly/flyctl-actions/setup-flyctl@master
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          version: 0.0.525
 | 
				
			||||||
 | 
					      - id: fly_destroy
 | 
				
			||||||
 | 
					        run: node buildtools/fly-deploy.js destroy
 | 
				
			||||||
							
								
								
									
										142
									
								
								buildtools/fly-deploy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								buildtools/fly-deploy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,142 @@
 | 
				
			|||||||
 | 
					const util = require('util');
 | 
				
			||||||
 | 
					const childProcess = require('child_process');
 | 
				
			||||||
 | 
					const fs = require('fs/promises');
 | 
				
			||||||
 | 
					const {existsSync} = require('fs');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const exec = util.promisify(childProcess.exec);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const org = "grist-labs";
 | 
				
			||||||
 | 
					const expirationSec = 30 * 24 * 60 * 60;  // 30 days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getAppName = () => "grist-" + getBranchName().toLowerCase().replace(/[\W|_]+/g, '-');
 | 
				
			||||||
 | 
					const getVolumeName = () => "grist_vol_" + getBranchName().toLowerCase().replace(/\W+/g, '_');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getBranchName = () => {
 | 
				
			||||||
 | 
					  if (!process.env.BRANCH_NAME) { console.log('Usage: Need BRANCH_NAME env var'); process.exit(1); }
 | 
				
			||||||
 | 
					  return process.env.BRANCH_NAME;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
					  if (process.argv[2] === 'deploy') {
 | 
				
			||||||
 | 
					    const appRoot = process.argv[3] || ".";
 | 
				
			||||||
 | 
					    if (!existsSync(`${appRoot}/Dockerfile`)) {
 | 
				
			||||||
 | 
					      console.log(`Dockerfile not found in appRoot of ${appRoot}`);
 | 
				
			||||||
 | 
					      process.exit(1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const name = getAppName();
 | 
				
			||||||
 | 
					    const volName = getVolumeName();
 | 
				
			||||||
 | 
					    if (!await appExists(name)) {
 | 
				
			||||||
 | 
					      await appCreate(name);
 | 
				
			||||||
 | 
					      await appScale(name);
 | 
				
			||||||
 | 
					      await volCreate(name, volName);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // Check if volume exists, and create it if not. This is needed because there was an API
 | 
				
			||||||
 | 
					      // change in flyctl (mandatory -y flag) and some apps were created without a volume.
 | 
				
			||||||
 | 
					      if (!(await volList(name)).length) {
 | 
				
			||||||
 | 
					        await volCreate(name, volName);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await prepConfig(name, appRoot, volName);
 | 
				
			||||||
 | 
					    await appDeploy(name, appRoot);
 | 
				
			||||||
 | 
					  } else if (process.argv[2] === 'destroy') {
 | 
				
			||||||
 | 
					    const name = getAppName();
 | 
				
			||||||
 | 
					    if (await appExists(name)) {
 | 
				
			||||||
 | 
					      await appDestroy(name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (process.argv[2] === 'clean') {
 | 
				
			||||||
 | 
					    const staleApps = await findStaleApps();
 | 
				
			||||||
 | 
					    for (const appName of staleApps) {
 | 
				
			||||||
 | 
					      await appDestroy(appName);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    console.log(`Usage:
 | 
				
			||||||
 | 
					  deploy [appRoot]:
 | 
				
			||||||
 | 
					            create (if needed) and deploy fly app grist-{BRANCH_NAME}.
 | 
				
			||||||
 | 
					            appRoot may specify the working directory that contains the Dockerfile to build.
 | 
				
			||||||
 | 
					  destroy:  destroy fly app grist-{BRANCH_NAME}
 | 
				
			||||||
 | 
					  clean:    destroy all grist-* fly apps whose time has come
 | 
				
			||||||
 | 
					            (according to FLY_DEPLOY_EXPIRATION env var set at deploy time)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  DRYRUN=1 in environment will show what would be done
 | 
				
			||||||
 | 
					`);
 | 
				
			||||||
 | 
					    process.exit(1);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appExists = (name) => runFetch(`flyctl status -a ${name}`).then(() => true).catch(() => false);
 | 
				
			||||||
 | 
					const appCreate = (name) => runAction(`flyctl apps create ${name} -o ${org}`);
 | 
				
			||||||
 | 
					const appScale = (name) => runAction(`flyctl scale memory 1024 -a ${name}`);
 | 
				
			||||||
 | 
					const volCreate = (name, vol) => runAction(`flyctl volumes create ${vol} -s 1 -r ewr -y -a ${name}`);
 | 
				
			||||||
 | 
					const volList = (name) => runFetch(`flyctl volumes list -a ${name} -j`).then(({stdout}) => JSON.parse(stdout));
 | 
				
			||||||
 | 
					const appDeploy = (name, appRoot) => runAction(`flyctl deploy ${appRoot} --remote-only`, {shell: true, stdio: 'inherit'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function appDestroy(name) {
 | 
				
			||||||
 | 
					  await runAction(`flyctl apps destroy ${name} -y`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function prepConfig(name, appRoot, volName) {
 | 
				
			||||||
 | 
					  const configPath = `${appRoot}/fly.toml`;
 | 
				
			||||||
 | 
					  const configTemplatePath = `${appRoot}/buildtools/fly-template.toml`;
 | 
				
			||||||
 | 
					  const template = await fs.readFile(configTemplatePath, {encoding: 'utf8'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Calculate the time when we can destroy the app, used by findStaleApps.
 | 
				
			||||||
 | 
					  const expiration = new Date(Date.now() + expirationSec * 1000).toISOString();
 | 
				
			||||||
 | 
					  const config = template
 | 
				
			||||||
 | 
					    .replace(/{APP_NAME}/g, name)
 | 
				
			||||||
 | 
					    .replace(/{VOLUME_NAME}/g, volName)
 | 
				
			||||||
 | 
					    .replace(/{FLY_DEPLOY_EXPIRATION}/g, expiration);
 | 
				
			||||||
 | 
					  await fs.writeFile(configPath, config);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function runFetch(cmd) {
 | 
				
			||||||
 | 
					  console.log(`Running: ${cmd}`);
 | 
				
			||||||
 | 
					  return exec(cmd);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function runAction(cmd) {
 | 
				
			||||||
 | 
					  if (process.env.DRYRUN) {
 | 
				
			||||||
 | 
					    console.log(`Would run: ${cmd}`);
 | 
				
			||||||
 | 
					    return;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  console.log(`Running: ${cmd}`);
 | 
				
			||||||
 | 
					  const cp = childProcess.spawn(cmd, {shell: true, stdio: 'inherit'});
 | 
				
			||||||
 | 
					  return new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					    cp.on('error', reject);
 | 
				
			||||||
 | 
					    cp.on('exit', function (code) {
 | 
				
			||||||
 | 
					      if (code === 0) {
 | 
				
			||||||
 | 
					        resolve();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        reject(new Error(`exited with code ${code}`));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function findStaleApps() {
 | 
				
			||||||
 | 
					  const {stdout} = await runFetch(`flyctl apps list -j`);
 | 
				
			||||||
 | 
					  const list = JSON.parse(stdout);
 | 
				
			||||||
 | 
					  const appNames = [];
 | 
				
			||||||
 | 
					  for (const app of list) {
 | 
				
			||||||
 | 
					    if (app.Organization?.Slug !== org) {
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const {stdout} = await runFetch(`flyctl config display -a ${app.Name}`);
 | 
				
			||||||
 | 
					    const expiration = JSON.parse(stdout).env?.FLY_DEPLOY_EXPIRATION;
 | 
				
			||||||
 | 
					    if (!expiration) {
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const expired = (Date.now() > Number(new Date(expiration)));
 | 
				
			||||||
 | 
					    if (isNaN(expired)) {
 | 
				
			||||||
 | 
					      console.warn(`Skipping ${app.Name} with invalid expiration ${expiration}`);
 | 
				
			||||||
 | 
					    } else if (!expired) {
 | 
				
			||||||
 | 
					      console.log(`Skipping ${app.Name}; not reached expiration of ${expiration}`);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      console.log(`Will clean ${app.Name}; expired at ${expiration}`);
 | 
				
			||||||
 | 
					      appNames.push(app.Name);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return appNames;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main().catch(err => { console.warn("ERROR", err); process.exit(1); });
 | 
				
			||||||
							
								
								
									
										46
									
								
								buildtools/fly-template.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								buildtools/fly-template.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					app = "{APP_NAME}"
 | 
				
			||||||
 | 
					kill_signal = "SIGINT"
 | 
				
			||||||
 | 
					kill_timeout = 5
 | 
				
			||||||
 | 
					processes = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[env]
 | 
				
			||||||
 | 
					  APP_DOC_URL="https://{APP_NAME}.fly.dev"
 | 
				
			||||||
 | 
					  APP_HOME_URL="https://{APP_NAME}.fly.dev"
 | 
				
			||||||
 | 
					  APP_STATIC_URL="https://{APP_NAME}.fly.dev"
 | 
				
			||||||
 | 
					  GRIST_SINGLE_ORG="docs"
 | 
				
			||||||
 | 
					  PORT = "8080"
 | 
				
			||||||
 | 
					  FLY_DEPLOY_EXPIRATION = "{FLY_DEPLOY_EXPIRATION}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[experimental]
 | 
				
			||||||
 | 
					  allowed_public_ports = []
 | 
				
			||||||
 | 
					  auto_rollback = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[services]]
 | 
				
			||||||
 | 
					  http_checks = []
 | 
				
			||||||
 | 
					  internal_port = 8080
 | 
				
			||||||
 | 
					  processes = ["app"]
 | 
				
			||||||
 | 
					  protocol = "tcp"
 | 
				
			||||||
 | 
					  script_checks = []
 | 
				
			||||||
 | 
					  [services.concurrency]
 | 
				
			||||||
 | 
					    hard_limit = 25
 | 
				
			||||||
 | 
					    soft_limit = 20
 | 
				
			||||||
 | 
					    type = "connections"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [[services.ports]]
 | 
				
			||||||
 | 
					    force_https = true
 | 
				
			||||||
 | 
					    handlers = ["http"]
 | 
				
			||||||
 | 
					    port = 80
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [[services.ports]]
 | 
				
			||||||
 | 
					    handlers = ["tls", "http"]
 | 
				
			||||||
 | 
					    port = 443
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  [[services.tcp_checks]]
 | 
				
			||||||
 | 
					    grace_period = "1s"
 | 
				
			||||||
 | 
					    interval = "15s"
 | 
				
			||||||
 | 
					    restart_limit = 0
 | 
				
			||||||
 | 
					    timeout = "2s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[mounts]
 | 
				
			||||||
 | 
					source="{VOLUME_NAME}"
 | 
				
			||||||
 | 
					destination="/persist"
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user