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