Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
128 changes: 128 additions & 0 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: 🔍 PR Preview Deploy

on:
pull_request:
types: [opened, synchronize, reopened, closed]

env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

jobs:
deploy-pr:
name: 🚀 Deploy PR Preview
runs-on: ubuntu-latest
if: github.event.action != 'closed'

steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4

- name: 🎈 Setup Fly
uses: superfly/flyctl-actions/[email protected]

- name: 🏷️ Generate app name
id: app-name
run: |
# Create a unique app name for this PR
APP_NAME="kcd-pr-${{ github.event.number }}"
echo "app_name=$APP_NAME" >> $GITHUB_OUTPUT
echo "url=https://$APP_NAME.fly.dev" >> $GITHUB_OUTPUT

- name: 📝 Setup PR-specific configurations
run: |
# Make script executable and run it
chmod +x other/setup-pr-preview.js
node other/setup-pr-preview.js "${{ steps.app-name.outputs.app_name }}"

echo "=== Modified fly.toml ==="
cat fly.toml
echo "=== Modified litefs.yml ==="
cat other/litefs.yml

- name: 🏗️ Create PR app
run: |
flyctl apps create ${{ steps.app-name.outputs.app_name }} --org personal || echo "App already exists"

- name: 🚀 Deploy PR app
run: |
flyctl deploy --depot --remote-only --build-arg COMMIT_SHA=${{ github.sha }} --app ${{ steps.app-name.outputs.app_name }}

- name: 💬 Comment on PR
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.type === 'Bot' && comment.body.includes('PR Preview')
);

const commentBody = `🚀 **PR Preview Deployed**

Your pull request has been deployed to a temporary Fly machine:

🔗 **Preview URL**: ${{ steps.app-name.outputs.url }}
📱 **App Name**: \`${{ steps.app-name.outputs.app_name }}\`

This preview will automatically scale to zero when not in use to save costs.
The app will be automatically deleted when this PR is closed or merged.

---
<sub>Updated: ${new Date().toISOString()}</sub>`;

if (botComment) {
github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
}

cleanup-pr:
name: 🧹 Cleanup PR Preview
runs-on: ubuntu-latest
if: github.event.action == 'closed'

steps:
- name: 🎈 Setup Fly
uses: superfly/flyctl-actions/[email protected]

- name: 🏷️ Generate app name
id: app-name
run: |
APP_NAME="kcd-pr-${{ github.event.number }}"
echo "app_name=$APP_NAME" >> $GITHUB_OUTPUT

- name: 🗑️ Delete PR app
run: |
flyctl apps destroy ${{ steps.app-name.outputs.app_name }} --yes || echo "App doesn't exist or already deleted"

- name: 💬 Comment on PR
uses: actions/github-script@v7
with:
script: |
const commentBody = `🧹 **PR Preview Cleaned Up**

The temporary Fly machine for this PR has been deleted.

---
<sub>Cleaned up: ${new Date().toISOString()}</sub>`;

github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
97 changes: 97 additions & 0 deletions other/setup-pr-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

Check warning on line 4 in other/setup-pr-preview.js

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'path' is assigned a value but never used. Allowed unused vars must match /^ignored/u

function setupPRPreview() {
const appName = process.argv[2];
if (!appName) {
console.error('Usage: node setup-pr-preview.js <app-name>');
process.exit(1);
}

console.log(`Setting up PR preview for app: ${appName}`);

// Modify fly.toml
setupFlyToml(appName);

// Modify litefs.yml
setupLitefsYml();

console.log('PR preview configuration complete!');
}

function setupFlyToml(appName) {
const flyTomlPath = 'fly.toml';
const backupPath = 'fly.toml.backup';

console.log('Modifying fly.toml...');

// Create backup
fs.copyFileSync(flyTomlPath, backupPath);

// Read and modify fly.toml
let flyToml = fs.readFileSync(flyTomlPath, 'utf8');

// Update app name
flyToml = flyToml.replace(/^app = .*/m, `app = "${appName}"`);

// Remove consul from experimental section
flyToml = flyToml.replace(/^\s*enable_consul = true.*$/m, '');

// Add auto-scaling configuration to services section
// Find [[services]] section and add auto-scaling after internal_port
const servicesRegex = /(\[\[services\]\][\s\S]*?internal_port = \d+)/;
if (servicesRegex.test(flyToml)) {
flyToml = flyToml.replace(
servicesRegex,
`$1
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]`
);
}

// Write modified fly.toml
fs.writeFileSync(flyTomlPath, flyToml);
console.log('✓ fly.toml updated');
}

function setupLitefsYml() {
const litefsPath = 'other/litefs.yml';
const backupPath = 'other/litefs.yml.backup';

console.log('Modifying litefs.yml...');

// Create backup
fs.copyFileSync(litefsPath, backupPath);

// Read litefs.yml
let litefsYml = fs.readFileSync(litefsPath, 'utf8');

// Replace the lease section with static lease
// Find the lease section and replace it with static configuration
const leaseRegex = /^lease:[\s\S]*?(?=^exec:)/m;
const staticLeaseConfig = `lease:
type: 'static'

`;

litefsYml = litefsYml.replace(leaseRegex, staticLeaseConfig);

// Add explanatory comment before the lease section
litefsYml = litefsYml.replace(
/^lease:/m,
`# PR Preview: Using static lease type for standalone instance
# This prevents syncing with production data
lease:`
);

// Write modified litefs.yml
fs.writeFileSync(litefsPath, litefsYml);
console.log('✓ litefs.yml updated');
}

// Run the setup
setupPRPreview();
Loading