Skip to content

Commit e9f8df8

Browse files
ci: add Kamal CI/CD deployment workflow
- Disable Vercel deployments (deploymentEnabled: false) - Create .github/workflows/deploy.yml with full test suite - Deploy only after lint, type-check, unit-test, e2e-test pass - Add Docker layer caching (type=gha) for faster builds - Document required GitHub Secrets in docs/deployment.md Closes rb-s6j Amp-Thread-ID: https://ampcode.com/threads/T-019ba734-5d2d-76f9-af80-0cef9b28f467 Co-authored-by: Amp <amp@ampcode.com>
1 parent a0d64ea commit e9f8df8

3 files changed

Lines changed: 341 additions & 0 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Kamal deployment workflow
2+
# Deploys to production after all tests pass on push to main
3+
4+
name: Deploy
5+
6+
on:
7+
push:
8+
branches: [main]
9+
workflow_dispatch: # Allow manual deployment
10+
11+
jobs:
12+
# Run all tests before deploying
13+
cache-npm:
14+
name: Cache NPM libraries
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout repository
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Node.js
21+
uses: actions/setup-node@v4
22+
with:
23+
node-version: '24.x'
24+
25+
- name: Restore NPM cache
26+
uses: actions/cache@v4
27+
id: cache
28+
with:
29+
path: ~/.npm
30+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
31+
restore-keys: |
32+
${{ runner.os }}-node-
33+
34+
- name: Install locked dependencies
35+
if: steps.cache.outputs.cache-hit != 'true'
36+
run: npm ci
37+
38+
lint:
39+
name: ESLint
40+
runs-on: ubuntu-latest
41+
needs: cache-npm
42+
steps:
43+
- name: Checkout repository
44+
uses: actions/checkout@v4
45+
46+
- name: Setup Node.js
47+
uses: actions/setup-node@v4
48+
with:
49+
node-version: '24.x'
50+
51+
- name: Restore NPM cache
52+
uses: actions/cache@v4
53+
with:
54+
path: ~/.npm
55+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
56+
restore-keys: |
57+
${{ runner.os }}-node-
58+
59+
- name: Install locked dependencies
60+
run: npm ci
61+
62+
- name: Generate Prisma Client
63+
run: npx prisma generate
64+
65+
- name: Build CSS assets from Tailwind CSS
66+
run: npm run build:css
67+
68+
- name: Lint files
69+
run: npm run lint
70+
env:
71+
CI: true
72+
73+
type-check:
74+
name: Type check
75+
runs-on: ubuntu-latest
76+
needs: cache-npm
77+
steps:
78+
- name: Checkout repository
79+
uses: actions/checkout@v4
80+
81+
- name: Setup Node.js
82+
uses: actions/setup-node@v4
83+
with:
84+
node-version: '24.x'
85+
86+
- name: Restore NPM cache
87+
uses: actions/cache@v4
88+
with:
89+
path: ~/.npm
90+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
91+
restore-keys: |
92+
${{ runner.os }}-node-
93+
94+
- name: Install locked dependencies
95+
run: npm ci
96+
97+
- name: Generate Prisma Client
98+
run: npx prisma generate
99+
100+
- name: Type check
101+
run: npm run type-check
102+
env:
103+
CI: true
104+
105+
unit-test:
106+
name: Unit and integration test
107+
runs-on: ubuntu-latest
108+
needs: cache-npm
109+
steps:
110+
- name: Checkout repository
111+
uses: actions/checkout@v4
112+
113+
- name: Setup Node.js
114+
uses: actions/setup-node@v4
115+
with:
116+
node-version: '24.x'
117+
118+
- name: Restore NPM cache
119+
uses: actions/cache@v4
120+
with:
121+
path: ~/.npm
122+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
123+
restore-keys: |
124+
${{ runner.os }}-node-
125+
126+
- name: Install locked dependencies
127+
run: npm ci
128+
129+
- name: Run unit and integration tests
130+
run: npm t
131+
env:
132+
CI: true
133+
134+
e2e-test:
135+
name: End-to-end test
136+
runs-on: ubuntu-latest
137+
needs: cache-npm
138+
steps:
139+
- name: Checkout repository
140+
uses: actions/checkout@v4
141+
142+
- name: Setup Node.js
143+
uses: actions/setup-node@v4
144+
with:
145+
node-version: '24.x'
146+
147+
- name: Restore NPM cache
148+
uses: actions/cache@v4
149+
with:
150+
path: ~/.npm
151+
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/playwright.config.ts') }}
152+
restore-keys: |
153+
${{ runner.os }}-node-
154+
155+
- name: Install locked dependencies
156+
run: npm ci
157+
158+
- name: Restore cached Playwright dependencies
159+
id: cache-playwright
160+
uses: actions/cache@v4
161+
with:
162+
path: ~/.cache/ms-playwright
163+
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/playwright.config.ts') }}
164+
restore-keys: |
165+
${{ runner.os }}-playwright-
166+
167+
- name: Install Playwright browsers with dependencies
168+
if: steps.cache-playwright.outputs.cache-hit != 'true'
169+
run: npx playwright install --with-deps
170+
171+
- name: Install Playwright system dependencies (on cache hit)
172+
if: steps.cache-playwright.outputs.cache-hit == 'true'
173+
run: npx playwright install-deps
174+
175+
- name: Run end-to-end tests
176+
run: npm run test:e2e:run
177+
env:
178+
CI: true
179+
SESSION_SECRET: ${{ secrets.SESSION_SECRET }}
180+
MAGIC_LINK_SECRET: ${{ secrets.MAGIC_LINK_SECRET }}
181+
MAILGUN_SENDING_KEY: nothing
182+
MAILGUN_DOMAIN: nothing
183+
184+
deploy:
185+
name: Deploy to production
186+
runs-on: ubuntu-latest
187+
needs: [lint, type-check, unit-test, e2e-test]
188+
# Only run on push to main (not on PRs)
189+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
190+
191+
steps:
192+
- name: Checkout repository
193+
uses: actions/checkout@v4
194+
195+
- name: Set up Docker Buildx
196+
uses: docker/setup-buildx-action@v3
197+
198+
- name: Login to GitHub Container Registry
199+
uses: docker/login-action@v3
200+
with:
201+
registry: ghcr.io
202+
username: ${{ github.actor }}
203+
password: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
204+
205+
- name: Build and push Docker image
206+
uses: docker/build-push-action@v6
207+
with:
208+
context: .
209+
push: true
210+
tags: |
211+
ghcr.io/zainfathoni/kelas.rumahberbagi.com:${{ github.sha }}
212+
ghcr.io/zainfathoni/kelas.rumahberbagi.com:latest
213+
cache-from: type=gha
214+
cache-to: type=gha,mode=max
215+
platforms: linux/amd64
216+
217+
- name: Set up Ruby for Kamal
218+
uses: ruby/setup-ruby@v1
219+
with:
220+
ruby-version: '3.3'
221+
222+
- name: Install Kamal
223+
run: gem install kamal
224+
225+
- name: Set up SSH key
226+
run: |
227+
mkdir -p ~/.ssh
228+
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
229+
chmod 600 ~/.ssh/id_rsa
230+
ssh-keyscan -H 103.235.75.227 >> ~/.ssh/known_hosts
231+
232+
- name: Create Kamal secrets file
233+
run: |
234+
mkdir -p .kamal
235+
cat > .kamal/secrets << EOF
236+
KAMAL_REGISTRY_PASSWORD=${{ secrets.KAMAL_REGISTRY_PASSWORD }}
237+
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
238+
MAGIC_LINK_SECRET=${{ secrets.MAGIC_LINK_SECRET }}
239+
MAILGUN_SENDING_KEY=${{ secrets.MAILGUN_SENDING_KEY }}
240+
MAILGUN_DOMAIN=${{ secrets.MAILGUN_DOMAIN }}
241+
EOF
242+
243+
- name: Deploy with Kamal
244+
run: kamal deploy --skip-push --version ${{ github.sha }}
245+
env:
246+
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}

docs/deployment.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Deployment
2+
3+
This project uses [Kamal 2.0](https://kamal-deploy.org/) for production
4+
deployment.
5+
6+
## Overview
7+
8+
- **Production URL**: <https://kelas.rumahberbagi.com>
9+
- **VPS**: 103.235.75.227 (Jetorbit)
10+
- **Container Registry**: GitHub Container Registry (ghcr.io)
11+
- **SSL**: Managed by kamal-proxy (Let's Encrypt)
12+
13+
## CI/CD Pipeline
14+
15+
Deployment is automated via GitHub Actions:
16+
17+
1. Push to `main` branch triggers the deploy workflow
18+
2. All tests must pass (lint, type-check, unit tests, E2E tests)
19+
3. Docker image is built with layer caching
20+
4. Image is pushed to GitHub Container Registry
21+
5. Kamal deploys to production VPS
22+
23+
## Required GitHub Secrets
24+
25+
Configure these secrets in your repository settings
26+
(`Settings > Secrets and variables > Actions`):
27+
28+
| Secret | Description |
29+
| ------------------------- | -------------------------------------------------------- |
30+
| `SSH_PRIVATE_KEY` | SSH private key for accessing the VPS |
31+
| `KAMAL_REGISTRY_PASSWORD` | GitHub Personal Access Token with `write:packages` scope |
32+
| `SESSION_SECRET` | Session encryption secret |
33+
| `MAGIC_LINK_SECRET` | Magic link email authentication secret |
34+
| `MAILGUN_SENDING_KEY` | Mailgun API key for sending emails |
35+
| `MAILGUN_DOMAIN` | Mailgun domain (e.g., `mg.rumahberbagi.com`) |
36+
37+
### SSH Key Setup
38+
39+
1. Generate an SSH key pair (if not already done):
40+
41+
```bash
42+
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/kamal_deploy
43+
```
44+
45+
2. Add the public key to the VPS:
46+
47+
```bash
48+
ssh-copy-id -i ~/.ssh/kamal_deploy.pub root@103.235.75.227
49+
```
50+
51+
3. Add the private key content to GitHub secret `SSH_PRIVATE_KEY`
52+
53+
### GitHub Container Registry Token
54+
55+
1. Create a Personal Access Token at <https://github.com/settings/tokens>
56+
2. Select scope: `write:packages`
57+
3. Add the token to GitHub secret `KAMAL_REGISTRY_PASSWORD`
58+
59+
## Manual Deployment
60+
61+
Deploy manually from your local machine:
62+
63+
```bash
64+
# Set up secrets in .kamal/secrets (see .kamal/secrets.example)
65+
kamal deploy
66+
```
67+
68+
## Configuration Files
69+
70+
- `config/deploy.yml` - Kamal deployment configuration
71+
- `.github/workflows/deploy.yml` - CI/CD workflow
72+
- `Dockerfile` - Multi-stage Docker build
73+
74+
## Rollback
75+
76+
To rollback to a previous version:
77+
78+
```bash
79+
kamal rollback <version>
80+
```
81+
82+
## Logs
83+
84+
View application logs:
85+
86+
```bash
87+
kamal app logs
88+
```
89+
90+
## Database Backups
91+
92+
See [backup-setup.md](backup-setup.md) for database backup configuration.

vercel.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
{
2+
"git": {
3+
"deploymentEnabled": false
4+
},
25
"build": {
36
"env": {
47
"ENABLE_FILE_SYSTEM_API": "1"

0 commit comments

Comments
 (0)