Skip to content

Commit 949cbc7

Browse files
Merge pull request #3 from jacksonlester/staging
Add staging environment support to data pipeline
2 parents 0ee0eb0 + 68b2928 commit 949cbc7

6 files changed

Lines changed: 386 additions & 21 deletions

File tree

.github/workflows/rebuild-cache.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ name: Rebuild Cache
33
on:
44
# Trigger manually from GitHub UI
55
workflow_dispatch:
6+
inputs:
7+
environment:
8+
description: 'Environment to deploy to'
9+
required: true
10+
default: 'staging'
11+
type: choice
12+
options:
13+
- staging
14+
- production
615

716
# Trigger when events.csv or geometries are updated
817
push:
918
branches:
1019
- main
20+
- staging
1121
paths:
1222
- 'events.csv'
1323
- 'geometries/**'
@@ -20,6 +30,20 @@ jobs:
2030
- name: Checkout code
2131
uses: actions/checkout@v4
2232

33+
- name: Determine environment
34+
id: env
35+
run: |
36+
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
37+
echo "STAGING=${{ github.event.inputs.environment == 'staging' && 'true' || 'false' }}" >> $GITHUB_OUTPUT
38+
echo "ENV_NAME=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
39+
elif [ "${{ github.ref }}" == "refs/heads/staging" ]; then
40+
echo "STAGING=true" >> $GITHUB_OUTPUT
41+
echo "ENV_NAME=staging" >> $GITHUB_OUTPUT
42+
else
43+
echo "STAGING=false" >> $GITHUB_OUTPUT
44+
echo "ENV_NAME=production" >> $GITHUB_OUTPUT
45+
fi
46+
2347
- name: Setup Node.js
2448
uses: actions/setup-node@v4
2549
with:
@@ -32,4 +56,7 @@ jobs:
3256
env:
3357
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
3458
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
35-
run: node rebuild-cache.js
59+
STAGING: ${{ steps.env.outputs.STAGING }}
60+
run: |
61+
echo "Rebuilding ${{ steps.env.outputs.ENV_NAME }} cache..."
62+
node rebuild-cache.js

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
This dataset tracks autonomous vehicle deployments and powers [avmap.io](https://avmap.io). While currently focused on US services, we accept data from anywhere.
44

5+
## ⚠️ Important: Submit PRs to `staging` branch
6+
7+
**All pull requests should target the `staging` branch, not `main`.** This allows us to test your changes on the staging site before promoting to production.
8+
9+
1. Fork this repository
10+
2. Create a feature branch from `staging`
11+
3. Make your changes
12+
4. Submit a PR to the `staging` branch
13+
5. Your changes will be tested on the staging environment
14+
6. Once verified, they'll be promoted to production
15+
516
## Event sourcing basics
617

718
Each row in `events.csv` represents one change to a service. Service creation events include all attributes. **Update events must include the complete new state for the field being updated** (not deltas) and always include the `company` field to identify which service is being updated.

README.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Open dataset tracking autonomous vehicle deployments in the United States. Power
44

55
## What's here
66

7-
**events.csv** - Timeline of AV service changes since 2017 (launches, expansions, shutdowns, policy changes)
7+
**events.csv** - Timeline of AV service changes since 2017 (launches, expansions, shutdowns, policy changes)
88
**geometries/** - GeoJSON service area boundaries showing how coverage evolved
99

1010
This uses event sourcing: create events capture full service details, updates only record what changed. Every change has a source.
@@ -27,12 +27,76 @@ Load the GeoJSON files into any mapping tool. Parse the CSV for historical analy
2727

2828
## Contributing
2929

30-
Add events to `events.csv`, run `python3 scripts/validate.py`, submit a PR.
30+
**Important: Submit all PRs to the `staging` branch, not `main`.**
31+
32+
1. Fork this repository
33+
2. Create a feature branch from `staging`
34+
3. Add events to `events.csv` or update geometries
35+
4. Run `python3 scripts/validate.py` to verify your changes
36+
5. Submit a PR to the `staging` branch
37+
6. Your changes will be tested on the staging site before being promoted to production
3138

3239
See [CONTRIBUTING.md](CONTRIBUTING.md) for the data format spec and examples.
3340

3441
Most useful contributions: new service launches, boundary updates, fleet size changes, policy announcements.
3542

43+
## Staging & Production Workflow
44+
45+
This repository uses a staging environment to test data updates before they go live:
46+
47+
- **`staging` branch** → Deploys to staging environment (test your changes here)
48+
- **`main` branch** → Deploys to production (avmap.io)
49+
50+
### Testing Your Changes
51+
52+
After submitting a PR to `staging`:
53+
1. Wait for the GitHub Action to rebuild the staging cache
54+
2. Check the staging site to verify your data looks correct
55+
3. Once approved, your PR will be merged to `staging`
56+
4. Later, staging will be promoted to `main` (production)
57+
58+
## For Maintainers
59+
60+
### Promoting Staging to Production
61+
62+
Once staging data has been verified and looks good:
63+
64+
```bash
65+
# Create a PR from staging → main
66+
gh pr create --base main --head staging --title "Promote staging to production"
67+
68+
# Or via GitHub UI:
69+
# 1. Go to Pull Requests → New PR
70+
# 2. Base: main ← Compare: staging
71+
# 3. Review changes, then merge
72+
# 4. GitHub Action will automatically rebuild production cache
73+
```
74+
75+
### Syncing Production Data to Staging
76+
77+
To refresh staging with the latest production data:
78+
79+
```bash
80+
# Sync database tables
81+
node sync-prod-to-staging.js
82+
83+
# Copy geometry files
84+
node copy-geometries.js
85+
86+
# Rebuild staging cache
87+
STAGING=true node rebuild-cache.js
88+
```
89+
90+
### Manual Cache Rebuilds
91+
92+
```bash
93+
# Rebuild staging cache
94+
STAGING=true node rebuild-cache.js
95+
96+
# Rebuild production cache
97+
node rebuild-cache.js
98+
```
99+
36100
## License
37101

38102
MIT - use it however you want.

copy-geometries.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Copy Geometry Files from Production to Staging
5+
*
6+
* This script copies all GeoJSON files from the production bucket to staging:
7+
* - service-area-boundaries → staging-service-area-boundaries
8+
*
9+
* Run this after sync-prod-to-staging.js to ensure staging has all geometry files.
10+
*
11+
* Usage: node copy-geometries.js
12+
*/
13+
14+
import { createClient } from '@supabase/supabase-js'
15+
import { config } from 'dotenv'
16+
17+
// Load .env file
18+
if (!process.env.GITHUB_ACTIONS) {
19+
config()
20+
}
21+
22+
const supabaseUrl = process.env.SUPABASE_URL
23+
const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY
24+
25+
if (!supabaseUrl || !supabaseServiceKey) {
26+
console.error('❌ Missing required environment variables!')
27+
console.error('Please set SUPABASE_URL and SUPABASE_SERVICE_KEY in .env')
28+
process.exit(1)
29+
}
30+
31+
const supabase = createClient(supabaseUrl, supabaseServiceKey)
32+
33+
const PROD_BUCKET = 'service-area-boundaries'
34+
const STAGING_BUCKET = 'staging-service-area-boundaries'
35+
36+
async function copyGeometries() {
37+
console.log('🔄 Copying geometry files from production to staging...\n')
38+
39+
try {
40+
// Step 1: List all files in production bucket
41+
console.log(`📡 Listing files in ${PROD_BUCKET}...`)
42+
const { data: files, error: listError } = await supabase.storage
43+
.from(PROD_BUCKET)
44+
.list('', { limit: 1000 })
45+
46+
if (listError) throw listError
47+
48+
const geojsonFiles = files.filter(f => f.name.endsWith('.geojson'))
49+
console.log(` Found ${geojsonFiles.length} geometry files\n`)
50+
51+
if (geojsonFiles.length === 0) {
52+
console.log('✅ No files to copy')
53+
return
54+
}
55+
56+
// Step 2: Copy each file
57+
console.log(`📥 Copying files to ${STAGING_BUCKET}...`)
58+
let successCount = 0
59+
let failCount = 0
60+
61+
for (const file of geojsonFiles) {
62+
try {
63+
// Download from production
64+
const { data: fileData, error: downloadError } = await supabase.storage
65+
.from(PROD_BUCKET)
66+
.download(file.name)
67+
68+
if (downloadError) throw downloadError
69+
70+
// Upload to staging
71+
const { error: uploadError } = await supabase.storage
72+
.from(STAGING_BUCKET)
73+
.upload(file.name, fileData, {
74+
contentType: 'application/json',
75+
upsert: true // Overwrite if exists
76+
})
77+
78+
if (uploadError) throw uploadError
79+
80+
console.log(` ✅ ${file.name}`)
81+
successCount++
82+
83+
} catch (error) {
84+
console.error(` ❌ ${file.name}: ${error.message}`)
85+
failCount++
86+
}
87+
}
88+
89+
console.log(`\n📊 Copy Summary:`)
90+
console.log(` ✅ Successful: ${successCount}`)
91+
console.log(` ❌ Failed: ${failCount}`)
92+
console.log(` 📁 Total: ${geojsonFiles.length}`)
93+
94+
if (failCount === 0) {
95+
console.log('\n✅ All geometry files copied successfully!')
96+
console.log('\n📝 Next steps:')
97+
console.log(' Run: STAGING=true node rebuild-cache.js')
98+
} else {
99+
console.error('\n⚠️ Some files failed to copy. Check errors above.')
100+
process.exit(1)
101+
}
102+
103+
} catch (error) {
104+
console.error('❌ Copy failed:', error)
105+
process.exit(1)
106+
}
107+
}
108+
109+
copyGeometries()

rebuild-cache.js

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
#!/usr/bin/env node
22

33
/**
4-
* Production cache rebuild script
4+
* Cache rebuild script - supports both staging and production environments
55
*
66
* This script:
77
* 1. Fetches all data from Supabase database
88
* 2. Loads all geometry files from storage
99
* 3. Processes service areas using timeline logic
1010
* 4. Uploads combined JSON blob to storage
1111
*
12+
* Environment Detection:
13+
* - STAGING=true env var → Uses staging tables/buckets
14+
* - Default → Uses production tables/buckets
15+
*
1216
* Run this whenever you update av_events or service_area_geometries data
1317
*/
1418

1519
import { createClient } from '@supabase/supabase-js'
1620
import { config } from 'dotenv'
21+
import { execSync } from 'child_process'
1722

1823
// Load .env file for local development (GitHub Actions sets env vars directly)
1924
if (!process.env.GITHUB_ACTIONS) {
@@ -34,6 +39,26 @@ if (!supabaseUrl || !supabaseServiceKey) {
3439
process.exit(1)
3540
}
3641

42+
// Detect environment: staging or production
43+
const isStaging = process.env.STAGING === 'true'
44+
const environment = isStaging ? 'staging' : 'production'
45+
46+
// Environment-specific configuration
47+
const config_env = {
48+
eventsTable: isStaging ? 'av_events_staging' : 'av_events',
49+
geometriesTable: isStaging ? 'service_area_geometries_staging' : 'service_area_geometries',
50+
dataCacheBucket: isStaging ? 'staging-data-cache' : 'data-cache',
51+
geometriesBucket: isStaging ? 'staging-service-area-boundaries' : 'service-area-boundaries'
52+
}
53+
54+
console.log(`🌍 Environment: ${environment.toUpperCase()}`)
55+
console.log(`📋 Configuration:`)
56+
console.log(` Events table: ${config_env.eventsTable}`)
57+
console.log(` Geometries table: ${config_env.geometriesTable}`)
58+
console.log(` Data cache bucket: ${config_env.dataCacheBucket}`)
59+
console.log(` Geometries bucket: ${config_env.geometriesBucket}`)
60+
console.log('')
61+
3762
const supabase = createClient(supabaseUrl, supabaseServiceKey)
3863

3964
async function rebuildCache() {
@@ -45,13 +70,13 @@ async function rebuildCache() {
4570
console.log('📡 Fetching database data...')
4671
const [eventsResult, geometriesResult] = await Promise.all([
4772
supabase
48-
.from('av_events')
73+
.from(config_env.eventsTable)
4974
.select('*')
5075
.eq('aggregate_type', 'service_area')
5176
.order('event_date', { ascending: true }),
5277

5378
supabase
54-
.from('service_area_geometries')
79+
.from(config_env.geometriesTable)
5580
.select('*')
5681
.order('created_at', { ascending: false })
5782
])
@@ -149,35 +174,41 @@ async function rebuildCache() {
149174
const jsonData = JSON.stringify(cacheData)
150175
const sizeMB = (jsonData.length / 1024 / 1024).toFixed(2)
151176

152-
// Use timestamped filename to avoid caching issues
153-
const timestamp = Date.now();
154-
const filename = `all-data-${timestamp}.json`;
177+
// Use timestamped filename to avoid caching issues (production only)
178+
if (!isStaging) {
179+
const timestamp = Date.now();
180+
const filename = `all-data-${timestamp}.json`;
155181

156-
const { error: uploadError } = await supabase.storage
157-
.from('data-cache')
158-
.upload(filename, jsonData, {
159-
contentType: 'application/json',
160-
cacheControl: 'max-age=0, no-cache'
161-
})
182+
const { error: uploadError } = await supabase.storage
183+
.from(config_env.dataCacheBucket)
184+
.upload(filename, jsonData, {
185+
contentType: 'application/json',
186+
cacheControl: 'max-age=0, no-cache'
187+
})
162188

163-
if (uploadError) throw uploadError
189+
if (uploadError) throw uploadError
190+
console.log(` Backup saved: ${filename}`)
191+
}
164192

165-
// Also upload as all-data.json for backwards compatibility
166-
await supabase.storage.from('data-cache').remove(['all-data.json'])
193+
// Upload as all-data.json
194+
await supabase.storage.from(config_env.dataCacheBucket).remove(['all-data.json'])
167195
const { error: mainUploadError } = await supabase.storage
168-
.from('data-cache')
196+
.from(config_env.dataCacheBucket)
169197
.upload('all-data.json', jsonData, {
170198
contentType: 'application/json',
171199
cacheControl: 'max-age=0, no-cache'
172200
})
173201

174-
if (uploadError) throw uploadError
202+
if (mainUploadError) throw mainUploadError
175203

176204
const totalTime = Date.now() - startTime
205+
const publicUrl = `${supabaseUrl}/storage/v1/object/public/${config_env.dataCacheBucket}/all-data.json`
206+
177207
console.log(`🎉 Cache rebuild complete!`)
208+
console.log(` Environment: ${environment}`)
178209
console.log(` Size: ${sizeMB}MB`)
179210
console.log(` Time: ${totalTime}ms`)
180-
console.log(` URL: https://vbqijqcveavjycsfoszy.supabase.co/storage/v1/object/public/data-cache/all-data.json`)
211+
console.log(` URL: ${publicUrl}`)
181212

182213
if (failed.length > 0) {
183214
console.log(`⚠️ ${failed.length} geometries failed to load`)

0 commit comments

Comments
 (0)