Skip to content

Commit bc2bd88

Browse files
authored
feat: ✨ Additional features to Vercel transition (#751)
1 parent 9d57666 commit bc2bd88

6 files changed

Lines changed: 529 additions & 53 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
name: Cleanup stale preview environments
2+
3+
on:
4+
schedule:
5+
- cron: '0 2 * * 1' # Every Monday at 2am UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
find-stale:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
deployments: write
14+
outputs:
15+
stale_envs: ${{ steps.find.outputs.stale_envs }}
16+
17+
steps:
18+
- name: Find stale preview environments
19+
id: find
20+
uses: actions/github-script@v7
21+
with:
22+
github-token: ${{ secrets.GITHUB_TOKEN }}
23+
script: |
24+
const STALE_DAYS = 90;
25+
const STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1000;
26+
const ENV_PREFIX = 'fairdataihub-website-';
27+
const now = Date.now();
28+
29+
function slugify(branch) {
30+
return branch
31+
.toLowerCase()
32+
.replace(/[^a-z0-9-]/g, '-')
33+
.replace(/-+/g, '-')
34+
.replace(/^-|-$/g, '');
35+
}
36+
37+
// List all branches
38+
const branches = await github.paginate(github.rest.repos.listBranches, {
39+
owner: context.repo.owner,
40+
repo: context.repo.repo,
41+
});
42+
43+
// Build map: expected environment name → branch object
44+
const branchByEnv = new Map();
45+
for (const branch of branches) {
46+
const envName = `${ENV_PREFIX}${slugify(branch.name)}`;
47+
branchByEnv.set(envName, branch);
48+
}
49+
50+
// Get all GitHub environments
51+
const { data: envData } = await github.rest.repos.getAllEnvironments({
52+
owner: context.repo.owner,
53+
repo: context.repo.repo,
54+
});
55+
56+
const previewEnvs = (envData.environments || []).filter(e =>
57+
e.name.startsWith(ENV_PREFIX)
58+
);
59+
60+
const staleEnvs = [];
61+
62+
for (const env of previewEnvs) {
63+
const branch = branchByEnv.get(env.name);
64+
65+
if (!branch) {
66+
core.info(`${env.name}: branch no longer exists → stale`);
67+
staleEnvs.push(env.name);
68+
continue;
69+
}
70+
71+
// Check last commit date on the branch
72+
const { data: commit } = await github.rest.repos.getCommit({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
ref: branch.commit.sha,
76+
});
77+
78+
const lastPush = new Date(commit.commit.committer.date).getTime();
79+
const ageDays = Math.floor((now - lastPush) / (24 * 60 * 60 * 1000));
80+
81+
if (now - lastPush > STALE_MS) {
82+
core.info(`${env.name}: last push ${ageDays} days ago → stale`);
83+
staleEnvs.push(env.name);
84+
} else {
85+
core.info(`${env.name}: last push ${ageDays} days ago → active`);
86+
}
87+
}
88+
89+
core.setOutput('stale_envs', JSON.stringify(staleEnvs));
90+
core.info(`Found ${staleEnvs.length} stale environment(s)`);
91+
92+
cleanup:
93+
needs: find-stale
94+
runs-on: ubuntu-latest
95+
if: needs.find-stale.outputs.stale_envs != '[]'
96+
permissions:
97+
contents: read
98+
deployments: write
99+
100+
env:
101+
KAMAL_REGISTRY_LOGIN_SERVER: ${{ secrets.KAMAL_REGISTRY_LOGIN_SERVER }}
102+
KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
103+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
104+
KAMAL_SERVER_IP: ${{ secrets.KAMAL_SERVER_IP }}
105+
106+
steps:
107+
- uses: actions/checkout@v4
108+
109+
- uses: ruby/setup-ruby@v1
110+
with:
111+
ruby-version: 3.3.1
112+
bundler-cache: true
113+
114+
- run: gem install kamal
115+
116+
- uses: webfactory/ssh-agent@v0.9.0
117+
with:
118+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
119+
120+
- name: Remove stale Kamal preview deployments
121+
run: |
122+
echo '${{ needs.find-stale.outputs.stale_envs }}' | jq -r '.[]' | while read -r env_name; do
123+
branch_slug="${env_name#fairdataihub-website-}"
124+
export KAMAL_APP_NAME="$env_name"
125+
export KAMAL_APP_DOMAIN="website-${branch_slug}.fairdataihub.org"
126+
echo "Removing stale preview: $env_name"
127+
kamal remove -y -d preview || echo "Warning: kamal remove failed for $env_name, continuing..."
128+
done
129+
130+
- name: Set up crane
131+
uses: imjasonh/setup-crane@v0.4
132+
133+
- name: Delete stale registry images
134+
run: |
135+
crane auth login "$KAMAL_REGISTRY_LOGIN_SERVER" \
136+
-u "$KAMAL_REGISTRY_USERNAME" \
137+
-p "$KAMAL_REGISTRY_PASSWORD"
138+
139+
echo '${{ needs.find-stale.outputs.stale_envs }}' | jq -r '.[]' | while read -r env_name; do
140+
echo "Deleting registry image: $env_name"
141+
crane ls "$KAMAL_REGISTRY_LOGIN_SERVER/$env_name" 2>/dev/null \
142+
| xargs -I{} crane delete "$KAMAL_REGISTRY_LOGIN_SERVER/$env_name:{}" 2>/dev/null \
143+
|| true
144+
done
145+
146+
- name: Deactivate and delete GitHub environments
147+
if: always()
148+
uses: actions/github-script@v7
149+
with:
150+
github-token: ${{ secrets.GITHUB_TOKEN }}
151+
script: |
152+
const staleEnvs = JSON.parse('${{ needs.find-stale.outputs.stale_envs }}');
153+
154+
for (const envName of staleEnvs) {
155+
try {
156+
const { data: deployments } = await github.rest.repos.listDeployments({
157+
owner: context.repo.owner,
158+
repo: context.repo.repo,
159+
environment: envName,
160+
per_page: 100,
161+
});
162+
163+
for (const d of deployments) {
164+
await github.rest.repos.createDeploymentStatus({
165+
owner: context.repo.owner,
166+
repo: context.repo.repo,
167+
deployment_id: d.id,
168+
state: 'inactive',
169+
});
170+
await github.rest.repos.deleteDeployment({
171+
owner: context.repo.owner,
172+
repo: context.repo.repo,
173+
deployment_id: d.id,
174+
});
175+
}
176+
177+
await github.rest.repos.deleteAnEnvironment({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
environment_name: envName,
181+
});
182+
183+
core.info(`Cleaned up: ${envName}`);
184+
} catch (e) {
185+
core.warning(`Failed to clean up ${envName}: ${e.message}`);
186+
}
187+
}

.github/workflows/deploy-app.yml

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ jobs:
1818
# Job for deploying the UI/frontend application
1919
deploy-ui:
2020
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
deployments: write
2124

2225
# Environment variables for Kamal deployment and container registry
2326
env:
@@ -33,34 +36,85 @@ jobs:
3336
# Step 1: Checkout the repository code
3437
- uses: actions/checkout@v4
3538

36-
# Step 2: Setup Ruby environment for Kamal deployment tool
39+
# Step 2: Create GitHub deployment record and mark as in_progress
40+
- name: Create GitHub deployment
41+
id: create-deployment
42+
continue-on-error: true
43+
uses: actions/github-script@v7
44+
with:
45+
github-token: ${{ secrets.GITHUB_TOKEN }}
46+
script: |
47+
const deployment = await github.rest.repos.createDeployment({
48+
owner: context.repo.owner,
49+
repo: context.repo.repo,
50+
ref: context.ref,
51+
environment: 'production',
52+
auto_merge: false,
53+
required_contexts: [],
54+
});
55+
56+
core.setOutput('deployment_id', deployment.data.id);
57+
58+
await github.rest.repos.createDeploymentStatus({
59+
owner: context.repo.owner,
60+
repo: context.repo.repo,
61+
deployment_id: deployment.data.id,
62+
state: 'in_progress',
63+
description: 'Deployment in progress',
64+
});
65+
66+
# Step 3: Setup Ruby environment for Kamal deployment tool
3767
- uses: ruby/setup-ruby@v1
3868
with:
3969
ruby-version: 3.3.1
4070
bundler-cache: true # Cache Ruby dependencies for faster builds
4171

42-
# Step 3: Install Kamal deployment tool
72+
# Step 4: Install Kamal deployment tool
4373
- run: gem install kamal
4474

45-
# Step 4: Setup SSH agent for server access
75+
# Step 5: Setup SSH agent for server access
4676
- uses: webfactory/ssh-agent@v0.9.0
4777
with:
4878
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
4979

50-
# Step 5: Setup Docker Buildx for better caching and performance
80+
# Step 6: Setup Docker Buildx for better caching and performance
5181
- name: Set up Docker Buildx for cache
5282
uses: docker/setup-buildx-action@v3
5383

54-
# Step 6: Verify Kamal installation
84+
# Step 7: Verify Kamal installation
5585
- run: kamal version
5686

57-
# Step 7: Release any existing deployment locks
87+
# Step 8: Release any existing deployment locks
5888
# This prevents conflicts when redeploying while a previous deployment is running
5989
- run: kamal lock release
6090
# Uncomment for verbose output during troubleshooting:
6191
# - run: kamal lock release --verbose
6292

63-
# Step 8: Initial Kamal setup (only needed once per server)
93+
# Step 9: Initial Kamal setup (only needed once per server)
6494
# Note: This step might need to be run manually on the server first time
6595
# to add the user to the docker group: `sudo usermod -aG docker $USER | newgrp docker | docker ps`
66-
- run: kamal setup
96+
- id: kamal-deploy
97+
run: kamal setup
98+
99+
# Step 10: Update GitHub deployment status based on outcome
100+
- name: Update GitHub deployment status
101+
if: always()
102+
uses: actions/github-script@v7
103+
with:
104+
github-token: ${{ secrets.GITHUB_TOKEN }}
105+
script: |
106+
const deploymentId = '${{ steps.create-deployment.outputs.deployment_id }}';
107+
if (!deploymentId) return;
108+
109+
const deploySucceeded = '${{ steps.kamal-deploy.outcome }}' === 'success';
110+
const statusPayload = {
111+
owner: context.repo.owner,
112+
repo: context.repo.repo,
113+
deployment_id: parseInt(deploymentId),
114+
state: deploySucceeded ? 'success' : 'failure',
115+
description: deploySucceeded ? 'Deployment succeeded' : 'Deployment failed',
116+
};
117+
if (deploySucceeded) {
118+
statusPayload.environment_url = `https://${process.env.KAMAL_APP_DOMAIN}`;
119+
}
120+
await github.rest.repos.createDeploymentStatus(statusPayload);

0 commit comments

Comments
 (0)