Skip to content

Commit 4e9b9be

Browse files
msfstefbalegas
andauthored
feat: Shared SST code for example deployments (#2270)
Co-authored-by: Valter Balegas <[email protected]>
1 parent 69586fd commit 4e9b9be

29 files changed

+713
-1422
lines changed

.github/workflows/deploy_all_examples.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ concurrency:
1010
jobs:
1111
deploy-examples:
1212
name: Deploy All Examples to Production
13-
environment: 'Production'
13+
environment: "Production"
1414
runs-on: ubuntu-latest
1515

1616
env:
17-
DEPLOY_ENV: 'production'
17+
DEPLOY_ENV: "production"
18+
SHARED_INFRA_VPC_ID: ${{ vars.SHARED_INFRA_VPC_ID }}
19+
SHARED_INFRA_CLUSTER_ARN: ${{ vars.SHARED_INFRA_CLUSTER_ARN }}
1820
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
1921
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}
2022
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}

.github/workflows/deploy_examples.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717

1818
env:
1919
DEPLOY_ENV: ${{ github.event_name == 'push' && 'production' || format('pr-{0}', github.event.number) }}
20+
SHARED_INFRA_VPC_ID: ${{ vars.SHARED_INFRA_VPC_ID }}
21+
SHARED_INFRA_CLUSTER_ARN: ${{ vars.SHARED_INFRA_CLUSTER_ARN }}
2022
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
2123
CLOUDFLARE_DEFAULT_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_DEFAULT_ACCOUNT_ID }}
2224
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -53,74 +55,86 @@ jobs:
5355
working-directory: ./examples/yjs
5456
run: |
5557
pnpm sst deploy --stage ${{ env.DEPLOY_ENV }}
58+
code=$!
5659
if [ -f ".sst/outputs.json" ]; then
5760
yjs=$(jq -r '.website' .sst/outputs.json)
5861
echo "yjs=$yjs" >> $GITHUB_ENV
5962
else
6063
echo "sst outputs file not found. Exiting."
6164
exit 123
6265
fi
66+
exit $code
6367
6468
- name: Deploy Linearlite Read Only
6569
working-directory: ./examples/linearlite-read-only
6670
run: |
6771
pnpm sst deploy --stage ${{ env.DEPLOY_ENV }}
72+
code=$!
6873
if [ -f ".sst/outputs.json" ]; then
6974
linearlite_read_only=$(jq -r '.website' .sst/outputs.json)
7075
echo "linearlite_read_only=$linearlite_read_only" >> $GITHUB_ENV
7176
else
7277
echo "sst outputs file not found. Exiting."
7378
exit 123
7479
fi
80+
exit $code
7581
7682
- name: Deploy Write Patterns example
7783
working-directory: ./examples/write-patterns
7884
run: |
7985
pnpm --filter @electric-sql/client --filter @electric-sql/experimental --filter @electric-sql/react run build
8086
pnpm sst deploy --stage ${{ env.DEPLOY_ENV }}
87+
code=$!
8188
if [ -f ".sst/outputs.json" ]; then
8289
writes=$(jq -r '.website' .sst/outputs.json)
8390
echo "writes=$writes" >> $GITHUB_ENV
8491
else
8592
echo "sst outputs file not found. Exiting."
8693
exit 123
8794
fi
95+
exit $code
8896
8997
- name: Deploy NextJs example
9098
working-directory: ./examples/nextjs
9199
run: |
92100
pnpm sst deploy --stage ${{ env.DEPLOY_ENV }}
101+
code=$!
93102
if [ -f ".sst/outputs.json" ]; then
94103
nextjs=$(jq -r '.website' .sst/outputs.json)
95104
echo "nextjs=$nextjs" >> $GITHUB_ENV
96105
else
97106
echo "sst outputs file not found. Exiting."
98107
exit 123
99108
fi
109+
exit $code
100110
101111
- name: Deploy TODO App example
102112
working-directory: ./examples/todo-app
103113
run: |
104114
pnpm sst deploy --stage ${{ env.DEPLOY_ENV }}
115+
code=$!
105116
if [ -f ".sst/outputs.json" ]; then
106117
todoapp=$(jq -r '.website' .sst/outputs.json)
107118
echo "todoapp=$todoapp" >> $GITHUB_ENV
108119
else
109120
echo "sst outputs file not found. Exiting."
110121
exit 123
111122
fi
123+
exit $code
112124
113125
- name: Deploy proxy-auth example
114126
working-directory: ./examples/proxy-auth
115127
run: |
116128
pnpm sst deploy --stage ${{ env.DEPLOY_ENV }}
129+
code=$!
117130
if [ -f ".sst/outputs.json" ]; then
118131
auth=$(jq -r '.website' .sst/outputs.json)
119132
echo "auth=$auth" >> $GITHUB_ENV
120133
else
121134
echo "sst outputs file not found. Exiting."
122135
exit 123
123136
fi
137+
exit $code
124138
125139
- name: Add comment to PR
126140
if: github.event_name == 'pull_request'

.github/workflows/teardown_examples_pr_stack.yml

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ jobs:
1313
name: Teardown Examples PR stack
1414
environment: Pull request
1515
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
example:
20+
[
21+
"yjs",
22+
"linearlite-read-only",
23+
"write-patterns",
24+
"nextjs",
25+
"todo-app",
26+
"proxy-auth",
27+
]
1628

1729
env:
1830
DEPLOY_ENV: ${{ github.event_name == 'push' && 'production' || format('pr-{0}', github.event.number) }}
@@ -45,15 +57,8 @@ jobs:
4557
restore-keys: |
4658
sst-cache-${{ runner.os }}
4759
48-
- name: Remove Linearlite
49-
working-directory: examples/linearlite-read-only
50-
run: |
51-
export PR_NUMBER=${{ github.event.number }}
52-
echo "Removing stage pr-$PR_NUMBER"
53-
pnpm sst remove --stage "pr-$PR_NUMBER"
54-
55-
- name: Remove NextJs example
56-
working-directory: examples/nextjs
60+
- name: Remove ${{ matrix.example }} example
61+
working-directory: ./examples/${{ matrix.example }}
5762
run: |
5863
export PR_NUMBER=${{ github.event.number }}
5964
echo "Removing stage pr-$PR_NUMBER"

.github/workflows/ts_test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ jobs:
155155
cache: pnpm
156156
- run: pnpm install --frozen-lockfile
157157
- run: pnpm -r --filter "$(jq '.name' -r package.json)^..." build
158+
- run: pnpm --if-present run prepare
158159
- run: pnpm --if-present run typecheck
159160
- run: pnpm --if-present run build
160161
- run: pnpm --if-present run test

examples/.shared/.eslintrc.cjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module.exports = {
2+
env: {
3+
browser: true,
4+
es2021: true,
5+
node: true,
6+
},
7+
extends: [
8+
`eslint:recommended`,
9+
`plugin:@typescript-eslint/recommended`,
10+
`plugin:prettier/recommended`,
11+
],
12+
parserOptions: {
13+
ecmaVersion: 2022,
14+
requireConfigFile: false,
15+
sourceType: `module`,
16+
ecmaFeatures: {
17+
jsx: true,
18+
},
19+
},
20+
parser: `@typescript-eslint/parser`,
21+
plugins: [`prettier`],
22+
rules: {
23+
quotes: [`error`, `backtick`],
24+
'no-unused-vars': `off`,
25+
'@typescript-eslint/no-unused-vars': [
26+
`error`,
27+
{
28+
argsIgnorePattern: `^_`,
29+
varsIgnorePattern: `^_`,
30+
caughtErrorsIgnorePattern: `^_`,
31+
},
32+
],
33+
},
34+
ignorePatterns: [`**/node_modules/**`, `.eslintrc.cjs`],
35+
}

examples/.shared/.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"semi": false,
3+
"singleQuote": true,
4+
"tabWidth": 2,
5+
"trailingComma": "es5"
6+
}

examples/.shared/lib/database.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { execSync } from 'node:child_process'
2+
import { createNeonDb, getNeonConnectionString } from './neon'
3+
4+
async function addDatabaseToElectric({
5+
dbUri,
6+
}: {
7+
dbUri: string
8+
}): Promise<{ id: string; source_secret: string }> {
9+
const adminApi = process.env.ELECTRIC_ADMIN_API
10+
const teamId = process.env.ELECTRIC_TEAM_ID
11+
12+
if (!adminApi || !teamId) {
13+
throw new Error(`ELECTRIC_ADMIN_API or ELECTRIC_TEAM_ID is not set`)
14+
}
15+
16+
const adminApiTokenId = process.env.ELECTRIC_ADMIN_API_TOKEN_ID
17+
const adminApiTokenSecret = process.env.ELECTRIC_ADMIN_API_TOKEN_SECRET
18+
if (!adminApiTokenId || !adminApiTokenSecret) {
19+
throw new Error(
20+
`ADMIN_API_TOKEN_CLIENT_ID or ADMIN_API_TOKEN_CLIENT_SECRET is not set`
21+
)
22+
}
23+
24+
const result = await fetch(`${adminApi}/v1/sources`, {
25+
method: `PUT`,
26+
headers: {
27+
'Content-Type': `application/json`,
28+
'CF-Access-Client-Id': adminApiTokenId,
29+
'CF-Access-Client-Secret': adminApiTokenSecret,
30+
},
31+
body: JSON.stringify({
32+
database_url: dbUri,
33+
region: `us-east-1`,
34+
team_id: teamId,
35+
}),
36+
})
37+
38+
if (!result.ok) {
39+
throw new Error(
40+
`Could not add database to Electric (${result.status}): ${await result.text()}`
41+
)
42+
}
43+
44+
return await result.json()
45+
}
46+
47+
function applyMigrations(
48+
dbUri: string,
49+
migrationsDir: string = `./db/migrations`
50+
) {
51+
execSync(`pnpm exec pg-migrations apply --directory ${migrationsDir}`, {
52+
env: {
53+
...process.env,
54+
DATABASE_URL: dbUri,
55+
},
56+
})
57+
}
58+
59+
export function createDatabaseForCloudElectric({
60+
dbName,
61+
migrationsDirectory,
62+
}: {
63+
dbName: string
64+
migrationsDirectory: string
65+
}) {
66+
const neonProjectId = process.env.NEON_PROJECT_ID
67+
if (!neonProjectId) {
68+
throw new Error(`NEON_PROJECT_ID is not set`)
69+
}
70+
71+
const project = neon.getProjectOutput({
72+
id: neonProjectId,
73+
})
74+
const { ownerName, dbName: resultingDbName } = createNeonDb({
75+
projectId: project.id,
76+
branchId: project.defaultBranchId,
77+
dbName,
78+
})
79+
80+
const databaseUri = getNeonConnectionString({
81+
project,
82+
roleName: ownerName,
83+
databaseName: resultingDbName,
84+
pooled: false,
85+
})
86+
const pooledDatabaseUri = getNeonConnectionString({
87+
project,
88+
roleName: ownerName,
89+
databaseName: resultingDbName,
90+
pooled: true,
91+
})
92+
93+
if (migrationsDirectory) {
94+
databaseUri.apply((uri) => applyMigrations(uri, migrationsDirectory))
95+
}
96+
97+
const electricInfo = databaseUri.apply((dbUri) =>
98+
addDatabaseToElectric({ dbUri })
99+
)
100+
101+
return {
102+
sourceId: electricInfo.id,
103+
sourceSecret: electricInfo.source_secret,
104+
databaseUri,
105+
pooledDatabaseUri,
106+
}
107+
}

examples/.shared/lib/infra.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export function getSharedCluster(serviceName: string): sst.aws.Cluster {
2+
const sharedInfraVpcId = process.env.SHARED_INFRA_VPC_ID
3+
const sharedInfraClusterArn = process.env.SHARED_INFRA_CLUSTER_ARN
4+
if (!sharedInfraVpcId || !sharedInfraClusterArn) {
5+
throw new Error(
6+
`SHARED_INFRA_VPC_ID or SHARED_INFRA_CLUSTER_ARN is not set`
7+
)
8+
}
9+
10+
return sst.aws.Cluster.get(`${serviceName}-cluster`, {
11+
id: sharedInfraClusterArn,
12+
vpc: sst.aws.Vpc.get(`${serviceName}-vpc`, sharedInfraVpcId),
13+
})
14+
}
15+
16+
export const isProduction = () =>
17+
$app.stage.toLocaleLowerCase() === `production`

0 commit comments

Comments
 (0)