Skip to content
This repository was archived by the owner on Mar 24, 2026. It is now read-only.

Commit 1a6c45a

Browse files
committed
feat(deployment): implement GitHub Actions CI/CD for Cloudflare Workers
Introduce a comprehensive CI/CD pipeline using GitHub Actions for automatic deployments to Cloudflare Workers. This setup includes environment-specific configurations, allowing seamless deployment to both development and production environments without the need for secret prefixing. The deployment process is streamlined through a new deploy script that detects the branch and sets the appropriate environment. Additionally, documentation has been updated to reflect the new setup and provide clear instructions for configuring GitHub Environments and managing secrets.
1 parent e32ba03 commit 1a6c45a

File tree

20 files changed

+8713
-3044
lines changed

20 files changed

+8713
-3044
lines changed

.github/workflows/deploy.yml

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
name: Deploy to Cloudflare Workers
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
12+
env:
13+
# renovate: datasource=node depName=node
14+
NODE_VERSION: 22.14.0
15+
PNPM_VERSION: 10.17.0
16+
jobs:
17+
deploy:
18+
name: Deploy to ${{ github.ref == 'refs/heads/main' && 'Production' || 'Development' }}
19+
runs-on: ubuntu-latest
20+
# Use GitHub Environments: 'prod' for main branch, 'dev' for PRs/other branches
21+
environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
22+
permissions:
23+
contents: read
24+
deployments: write
25+
pull-requests: write
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
29+
30+
- name: Setup Node.js
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: ${{ env.NODE_VERSION }}
34+
35+
- name: Setup pnpm
36+
uses: pnpm/action-setup@v4
37+
with:
38+
version: ${{ env.PNPM_VERSION }}
39+
40+
- name: Install dependencies
41+
run: pnpm install --frozen-lockfile
42+
43+
- name: Build application
44+
run: pnpm run build:all
45+
46+
- name: Set secrets in Cloudflare Worker (Prefixed)
47+
run: |
48+
ENV_TYPE="${{ github.ref == 'refs/heads/main' && 'PROD' || 'DEV' }}"
49+
echo "🔐 Setting $ENV_TYPE prefixed secrets in Cloudflare Worker..."
50+
# Set prefixed secrets so both dev and prod can coexist in the same worker
51+
# Runtime detection will select the correct prefix based on request host
52+
PREFIX="$ENV_TYPE"
53+
54+
# Sensitive secrets (use GitHub Secrets)
55+
echo "${{ secrets.QUICKBOOKS_CLIENT_ID }}" | pnpm exec wrangler secret put ${PREFIX}_QUICKBOOKS_CLIENT_ID
56+
echo "${{ secrets.QUICKBOOKS_CLIENT_SECRET }}" | pnpm exec wrangler secret put ${PREFIX}_QUICKBOOKS_CLIENT_SECRET
57+
echo "${{ secrets.QUICKBOOKS_REFRESH_TOKEN }}" | pnpm exec wrangler secret put ${PREFIX}_QUICKBOOKS_REFRESH_TOKEN || true
58+
echo "${{ secrets.QUICKBOOKS_REALM_ID }}" | pnpm exec wrangler secret put ${PREFIX}_QUICKBOOKS_REALM_ID || true
59+
echo "${{ secrets.QUICKBOOKS_ADMIN_KEY }}" | pnpm exec wrangler secret put ${PREFIX}_QUICKBOOKS_ADMIN_KEY || true
60+
echo "${{ secrets.GITHUB_TOKEN }}" | pnpm exec wrangler secret put ${PREFIX}_GITHUB_TOKEN || true
61+
echo "${{ secrets.MONDAY_API_KEY }}" | pnpm exec wrangler secret put ${PREFIX}_MONDAY_API_KEY || true
62+
63+
# Non-sensitive variables (use GitHub Variables - still set as Cloudflare secrets for prefixed runtime access)
64+
# Note: Even though not sensitive, we set as secrets to maintain prefix structure for environment isolation
65+
if [ -n "${{ vars.DISCORD_WEBHOOK_URL }}" ]; then
66+
echo "${{ vars.DISCORD_WEBHOOK_URL }}" | pnpm exec wrangler secret put ${PREFIX}_DISCORD_WEBHOOK_URL || true
67+
fi
68+
if [ -n "${{ vars.MONDAY_BOARD_ID }}" ]; then
69+
echo "${{ vars.MONDAY_BOARD_ID }}" | pnpm exec wrangler secret put ${PREFIX}_MONDAY_BOARD_ID || true
70+
fi
71+
if [ -n "${{ vars.QUICKBOOKS_ENVIRONMENT }}" ]; then
72+
echo "${{ vars.QUICKBOOKS_ENVIRONMENT }}" | pnpm exec wrangler secret put ${PREFIX}_QUICKBOOKS_ENVIRONMENT || true
73+
fi
74+
env:
75+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
76+
77+
- name: Deploy to Cloudflare Workers
78+
run: |
79+
echo "🚀 Deploying to ${{ github.ref == 'refs/heads/main' && 'PRODUCTION' || 'DEVELOPMENT' }} environment..."
80+
# Deploy to single worker "dev" (no --env flag = base worker)
81+
# Worker URL: dev.allthingslinux.workers.dev
82+
# Production: allthingslinux.org (custom route in wrangler.jsonc)
83+
# Secrets are prefixed (DEV_* / PROD_*) - runtime detection selects correct prefix
84+
pnpm exec opennextjs-cloudflare deploy
85+
env:
86+
# Only Cloudflare API token needed for deployment (secrets are set separately above)
87+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
88+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
89+
90+
- name: Create deployment comment
91+
if: github.event_name == 'pull_request'
92+
uses: peter-evans/create-or-update-comment@v5
93+
continue-on-error: true
94+
with:
95+
issue-number: ${{ github.event.pull_request.number }}
96+
body: |
97+
## 🚀 Deployment Status
98+
99+
**Environment:** ${{ github.ref == 'refs/heads/main' && 'Production' || 'Development' }}
100+
**Branch:** `${{ github.ref_name }}`
101+
**Commit:** `${{ github.sha }}`
102+
103+
**URLs:**
104+
- **Production:** [https://allthingslinux.org](https://allthingslinux.org)
105+
- **Development:** [https://dev.allthingslinux.workers.dev](https://dev.allthingslinux.workers.dev)
106+
107+
Deployment completed successfully! ✨

README.md

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,25 @@ pnpm run dev:all # Next.js + Wrangler + Trigger.dev
7575

7676
## 🚀 Deployment
7777

78-
### Automatic Deployments
78+
### Automatic Deployments (GitHub Actions CI/CD)
7979

80-
**Git-based deployments via Cloudflare Workers Builds:**
80+
**GitHub Actions with GitHub Environments** - Automatic deployments on push/PR:
8181

82-
| Branch | Environment | URL |
83-
| ------ | ----------- | ------------------------------------------------------------------------ |
84-
| `main` | Production | [allthingslinux.org](https://allthingslinux.org) |
85-
| `dev` | Development | [allthingslinux-dev.allthingslinux.workers.dev](https://allthingslinux-dev.allthingslinux.workers.dev) |
82+
| Branch | Environment | URL |
83+
| -------- | ----------- | ------------------------------------------------------------------------ |
84+
| `main` | Production | [allthingslinux.org](https://allthingslinux.org) |
85+
| PR/other | Development | [dev.allthingslinux.workers.dev](https://dev.allthingslinux.workers.dev) |
8686

87-
**Setup:** See [Workers Builds Setup Guide](docs/WORKERS_BUILDS_SETUP.md) for detailed configuration.
87+
**Setup:** See [GitHub Environments Setup Guide](docs/GITHUB_ENVIRONMENTS_SETUP.md) for detailed configuration.
8888

89-
Quick setup: Connect your GitHub repo in [Cloudflare Dashboard → Workers → Builds](https://dash.cloudflare.com/workers-and-pages) and configure:
90-
- **Production branch deploy:** `pnpm install && pnpm run build:all && pnpm exec opennextjs-cloudflare deploy -- --env prod`
91-
- **Non-production branch deploy:** `pnpm install && pnpm run build:all && pnpm exec opennextjs-cloudflare deploy -- --env dev`
89+
**Quick setup:**
90+
91+
1. Create GitHub Environments: `dev` and `prod` (Settings → Environments)
92+
2. Add secrets to each environment (see guide for required secrets)
93+
3. Push to any branch → Auto-deploys via GitHub Actions
94+
4. Merge to `main` → Auto-deploys to production
95+
96+
**Workflow:** `.github/workflows/deploy.yml` automatically handles branch detection and environment selection.
9297

9398
### Manual Deployments
9499

@@ -126,7 +131,19 @@ pnpm run preview
126131

127132
## 🔐 Secrets & Environment
128133

129-
### Quick Setup
134+
### CI/CD (GitHub Actions) - Recommended
135+
136+
**For automatic deployments**, use GitHub Environments with secrets:
137+
138+
1. **Set up GitHub Environments**: Create `dev` and `prod` environments (Settings → Environments)
139+
2. **Add secrets** to each environment (same secret names, different values per environment)
140+
3. **Secrets are automatically available** in GitHub Actions workflows
141+
142+
See [GitHub Environments Setup Guide](docs/GITHUB_ENVIRONMENTS_SETUP.md) for complete setup instructions.
143+
144+
### Manual Deployment (Local)
145+
146+
**For manual deployments from your local machine:**
130147

131148
```bash
132149
# 1. Copy templates for each environment
@@ -145,10 +162,11 @@ pnpm run secrets:prod # Production (uses .env.secrets.prod)
145162
### Security Notes
146163

147164
- **Never commit** `.env.secrets.*` (they're gitignored)
148-
- **Secrets are encrypted** and managed via `wrangler secret put`
165+
- **GitHub Environments** are the recommended way for CI/CD (secrets isolated per environment)
166+
- **Secrets are encrypted** and managed via `wrangler secret put` or GitHub Environments
149167
- **Use `.dev.vars`** only for non-sensitive local config
150168
- **Environment variables** are defined in `wrangler.jsonc` per environment
151-
- **Environment-specific secrets** automatically selected by upload scripts
169+
- **No prefixing needed**: GitHub Environments handle isolation automatically
152170

153171
## 📁 Project Structure
154172

app/api/quickbooks/admin-setup/route.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function GET(request: NextRequest) {
4242

4343
// Set the state cookie for CSRF validation
4444
const response = NextResponse.redirect(authUrl.toString());
45-
45+
4646
// Cookie settings: secure in production, work with both localhost and workers.dev
4747
const isSecure = protocol === 'https';
4848
response.cookies.set('qb_oauth_state', state, {
@@ -61,8 +61,14 @@ export async function GET(request: NextRequest) {
6161
console.log('[QuickBooks OAuth] Protocol:', protocol);
6262
console.log('[QuickBooks OAuth] Redirect URI:', redirectUri);
6363
console.log('[QuickBooks OAuth] State:', state.substring(0, 16) + '...');
64-
console.log('[QuickBooks OAuth] ⚠️ IMPORTANT: This redirect URI must be added to your QuickBooks app');
65-
console.log('[QuickBooks OAuth] ⚠️ Make sure it is in the "' + (environment === 'sandbox' ? 'Development' : 'Production') + '" environment tab');
64+
console.log(
65+
'[QuickBooks OAuth] ⚠️ IMPORTANT: This redirect URI must be added to your QuickBooks app'
66+
);
67+
console.log(
68+
'[QuickBooks OAuth] ⚠️ Make sure it is in the "' +
69+
(environment === 'sandbox' ? 'Development' : 'Production') +
70+
'" environment tab'
71+
);
6672

6773
return response;
6874
}

app/api/quickbooks/callback/route.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ export async function GET(request: NextRequest) {
6565

6666
if (!isValidState) {
6767
console.error('CSRF state validation failed', {
68-
storedState: storedState ? `[${storedState.substring(0, 8)}...]` : 'missing',
68+
storedState: storedState
69+
? `[${storedState.substring(0, 8)}...]`
70+
: 'missing',
6971
receivedState: state ? `[${state.substring(0, 8)}...]` : 'missing',
7072
allCookies: Array.from(cookies.getAll()).map((c) => c.name),
7173
});
72-
74+
7375
// Return helpful error page instead of JSON for better debugging
7476
const errorHtml = `<!DOCTYPE html>
7577
<html>
@@ -87,7 +89,7 @@ export async function GET(request: NextRequest) {
8789
<p><a href="/api/quickbooks/admin-setup?admin=${encodeURIComponent(env.QUICKBOOKS_ADMIN_KEY || '')}">Try again</a></p>
8890
</body>
8991
</html>`;
90-
92+
9193
return new NextResponse(errorHtml, {
9294
headers: { 'Content-Type': 'text/html' },
9395
status: 403,
@@ -144,8 +146,11 @@ export async function GET(request: NextRequest) {
144146
// Get Cloudflare environment if available
145147
// Uses getCloudflareContext() which is the recommended way in OpenNext Cloudflare
146148
const cfEnv = getCloudflareEnv();
147-
148-
console.log('[QuickBooks Callback] KV namespace available:', !!cfEnv?.KV_QUICKBOOKS);
149+
150+
console.log(
151+
'[QuickBooks Callback] KV namespace available:',
152+
!!cfEnv?.KV_QUICKBOOKS
153+
);
149154
console.log('[QuickBooks Callback] Attempting to save tokens...', {
150155
hasClientId: !!tokenData.clientId,
151156
hasClientSecret: !!tokenData.clientSecret,
@@ -158,12 +163,20 @@ export async function GET(request: NextRequest) {
158163
const saved = await saveTokens(tokenData, cfEnv);
159164

160165
if (saved) {
161-
console.log('[QuickBooks Callback] ✅ QuickBooks tokens saved (KV or Secrets API)');
166+
console.log(
167+
'[QuickBooks Callback] ✅ QuickBooks tokens saved (KV or Secrets API)'
168+
);
162169
} else {
163-
console.warn('[QuickBooks Callback] ⚠️ Tokens NOT saved to KV/Secrets (using environment variables)');
170+
console.warn(
171+
'[QuickBooks Callback] ⚠️ Tokens NOT saved to KV/Secrets (using environment variables)'
172+
);
164173
console.log('[QuickBooks Callback] 💡 To enable automatic token saving:');
165-
console.log('[QuickBooks Callback] 1. Ensure KV namespace is accessible, OR');
166-
console.log('[QuickBooks Callback] 2. Add CLOUDFLARE_API_TOKEN as a secret to enable automatic secret updates');
174+
console.log(
175+
'[QuickBooks Callback] 1. Ensure KV namespace is accessible, OR'
176+
);
177+
console.log(
178+
'[QuickBooks Callback] 2. Add CLOUDFLARE_API_TOKEN as a secret to enable automatic secret updates'
179+
);
167180
// Fallback for development/local environments - only log in development
168181
if (env.NODE_ENV === 'development') {
169182
console.log('');

app/api/quickbooks/route.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,20 @@ export async function GET(request: NextRequest) {
4242
// Get Cloudflare environment
4343
// Uses getCloudflareContext() which is the recommended way in OpenNext Cloudflare
4444
const cfEnv = getCloudflareEnv();
45-
46-
console.log('[QuickBooks API] Request received, KV namespace available:', !!cfEnv?.KV_QUICKBOOKS);
45+
46+
console.log(
47+
'[QuickBooks API] Request received, KV namespace available:',
48+
!!cfEnv?.KV_QUICKBOOKS
49+
);
4750

4851
// Fetch transactions with Cloudflare environment
4952
const transactions = await fetchQuickBooksTransactions(cfEnv);
50-
51-
console.log('[QuickBooks API] Returning', transactions.length, 'transactions');
53+
54+
console.log(
55+
'[QuickBooks API] Returning',
56+
transactions.length,
57+
'transactions'
58+
);
5259

5360
return NextResponse.json({
5461
success: true,
@@ -58,7 +65,10 @@ export async function GET(request: NextRequest) {
5865
});
5966
} catch (error) {
6067
console.error('[QuickBooks API] ❌ Error fetching QuickBooks data:', error);
61-
console.error('[QuickBooks API] Error stack:', error instanceof Error ? error.stack : 'No stack trace');
68+
console.error(
69+
'[QuickBooks API] Error stack:',
70+
error instanceof Error ? error.stack : 'No stack trace'
71+
);
6272

6373
return NextResponse.json(
6474
{

app/finance/page.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,13 @@ async function TransactionsTable() {
9292
// Construct absolute URL for fetch (required in server components)
9393
const headersList = await headers();
9494
const host = headersList.get('host') || 'localhost:3000';
95-
const protocol = headersList.get('x-forwarded-proto') ||
96-
headersList.get('x-forwarded-scheme') ||
97-
(host.includes('localhost') ? 'http' : 'https');
95+
const protocol =
96+
headersList.get('x-forwarded-proto') ||
97+
headersList.get('x-forwarded-scheme') ||
98+
(host.includes('localhost') ? 'http' : 'https');
9899
const baseUrl = `${protocol}://${host}`;
99100
const apiUrl = `${baseUrl}/api/quickbooks`;
100-
101+
101102
// Fetch transactions via API route that has access to Cloudflare KV
102103
const response = await fetch(apiUrl, {
103104
cache: 'no-store', // Always fetch fresh data - prevents static generation

components/ui/badge.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ const badgeVariants = cva(
2424
);
2525

2626
export interface BadgeProps
27-
extends React.HTMLAttributes<HTMLDivElement>,
27+
extends
28+
React.HTMLAttributes<HTMLDivElement>,
2829
VariantProps<typeof badgeVariants> {}
2930

3031
function Badge({ className, variant, ...props }: BadgeProps) {

components/ui/button.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const buttonVariants = cva(
3434
);
3535

3636
export interface ButtonProps
37-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37+
extends
38+
React.ButtonHTMLAttributes<HTMLButtonElement>,
3839
VariantProps<typeof buttonVariants> {
3940
asChild?: boolean;
4041
}

components/ui/loading-spinner.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React from 'react';
22
import { cn } from '@/lib/utils';
33

4-
export interface LoadingSpinnerProps
5-
extends React.HTMLAttributes<HTMLDivElement> {
4+
export interface LoadingSpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
65
size?: 'small' | 'medium' | 'large';
76
className?: string;
87
}

components/ui/sheet.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ const sheetVariants = cva(
5050
);
5151

5252
interface SheetContentProps
53-
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
53+
extends
54+
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
5455
VariantProps<typeof sheetVariants> {}
5556

5657
const SheetContent = React.forwardRef<

0 commit comments

Comments
 (0)