mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +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