ReviewOS

also looking at this

stacks/bunpress

fix(ci): deploy with encrypted production env

#82
Merged chrisbbreuer wants to merge agent/oidc-encrypted-production-env into main
6 files +89 -16
.env.productionadded+9-0
Changes to .env.production
@@ -0,0 +1,9 @@
1#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
2#/ versioned X25519 encryption for .env files /
3#/ [how it works](https://stacksjs.com/encryption) /
4#/----------------------------------------------------------/
5DOTENV_PUBLIC_KEY_PRODUCTION="x25519-public:MCowBQYDK2VuAyEAVflLYovF9BhnwsVpiWU85Ch4zzQqqqCP1x2F2s0AuSQ"
6
7AWS_DEFAULT_REGION="encrypted:v2:eyJ2IjoyLCJlcGsiOiJNQ293QlFZREsyVnVBeUVBR1ROZENaNVZHUTZ6MnVDWmR1U3pfRk1ZbldHaEZFamhkbXFTbmQwbEZWSSIsInNhbHQiOiIwaHFPYWtJdlU3ZWlTMElrTXhPMmFRIiwibm9uY2UiOiJEaVowRDBzbDdxSlBzYzRxIiwiY2lwaGVydGV4dCI6Ik1nQ002VGRhcWhKYSIsInRhZyI6InNGVFFETXF4QjFKTzhkVmxzcGZCcFEifQ"
8AWS_S3_BUCKET="encrypted:v2:eyJ2IjoyLCJlcGsiOiJNQ293QlFZREsyVnVBeUVBUDRJblRHa0ZzT212YW9qTHY2N1R1eVNPa3JiYl9rY19LeUFEVy1uS1FIUSIsInNhbHQiOiJacFEzZ251b2dTWFFJUnMyNzV1N1VnIiwibm9uY2UiOiJPTW5OWW1XaEJyMU93THRjIiwiY2lwaGVydGV4dCI6ImRZWWE4YU1uNDVoUC0tMTUiLCJ0YWciOiJUa1Jma0szZ3Y1aTBzNmp1eWFjVnZnIn0"
9AWS_CLOUDFRONT_DISTRIBUTION_ID="encrypted:v2:eyJ2IjoyLCJlcGsiOiJNQ293QlFZREsyVnVBeUVBdXRYc1VLRUZoeXRZUUdlRFpYV1pqdjVBby1nMEY5VUY1c0VDZ1d3bVZVcyIsInNhbHQiOiJ4VDFtN2NCUFFtTEpkcmh2ekFBSTBnIiwibm9uY2UiOiI5UjdGd2JXWFM1ckxES2l4IiwiY2lwaGVydGV4dCI6IlpaN0JjQm9kQXBQWU9SdGwxejgiLCJ0YWciOiJJaTFZSXUzcF9TMkg2Vm9oM00yY0VBIn0"
.github/workflows/ci.ymlmodified+25-16
Changes to .github/workflows/ci.yml
@@ -112,13 +112,12 @@ jobs:
112112 needs: [lint, typecheck, test]
113113 if: github.event_name == 'push' && github.ref == 'refs/heads/main'
114114 runs-on: ubuntu-latest
115 permissions:
116 contents: read
117 id-token: write
115118 concurrency:
116119 group: bunpress-org-production
117120 cancel-in-progress: false
118 env:
119 AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
120 AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
121 AWS_DEFAULT_REGION: us-east-1
122121
123122 steps:
124123 - uses: actions/checkout@v6
@@ -133,6 +132,26 @@ jobs:
133132 - name: Install dependencies
134133 run: bun install --frozen-lockfile
135134
135 - name: Configure short-lived AWS credentials
136 uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
137 with:
138 aws-region: us-east-1
139 role-to-assume: arn:aws:iam::923076644019:role/github-actions-bunpress-deploy
140
141 - name: Load encrypted production environment
142 shell: bash
143 run: |
144 set -euo pipefail
145 trap 'rm -f .env.keys' EXIT
146
147 aws ssm get-parameter \
148 --name /bunpress/production/env-key \
149 --with-decryption \
150 --query Parameter.Value \
151 --output text > .env.keys
152 chmod 600 .env.keys
153 bun run env:production
154
136155 - name: Build documentation
137156 run: bun run build:docs
138157
@@ -141,19 +160,9 @@ jobs:
141160 run: |
142161 set -euo pipefail
143162
144 distribution_id="$(
145 aws cloudfront list-distributions --output json \
146 | jq -r '[.DistributionList.Items[] | select((.Aliases.Items // []) | index("bunpress.org")) | .Id][0] // empty'
147 )"
148
149 if [[ -z "$distribution_id" ]]; then
150 echo "Could not find the CloudFront distribution for bunpress.org." >&2
151 exit 1
152 fi
153
154 aws s3 sync dist/.bunpress s3://bunpress-org \
163 aws s3 sync "dist/.bunpress" "s3://${AWS_S3_BUCKET}" \
155164 --delete \
156165 --cache-control 'max-age=3600, public'
157166 aws cloudfront create-invalidation \
158 --distribution-id "$distribution_id" \
167 --distribution-id "$AWS_CLOUDFRONT_DISTRIBUTION_ID" \
159168 --paths '/*'
.gitignoremodified+2-0
Changes to .gitignore
@@ -20,5 +20,7 @@ test/use-cases/**/output/**
2020test/use-cases/**/.gitignore
2121docs/.bunpress/
2222.env
23.env.keys
24!.env.production
2325/pantry
2426pantry
bun.lockmodified+3-0
Changes to bun.lock
@@ -6,6 +6,7 @@
66 "name": "bunpress-monorepo",
77 "devDependencies": {
88 "@stacksjs/bumpx": "^0.2.4",
9 "@stacksjs/env": "^0.70.371",
910 "@stacksjs/gitlint": "^0.1.5",
1011 "@stacksjs/logsmith": "^0.2.1",
1112 "bun-git-hooks": "^0.3.1",
@@ -67,6 +68,8 @@
6768
6869 "@stacksjs/dtsx": ["@stacksjs/dtsx@0.10.9", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-cg/hykWH47pcnhz9/9QQGAIj6U13BZktDQccO/AeJM4algoLBM9/AMhwnAjW6nnQ8D+petHvSBf3oPz+ACUE0Q=="],
6970
71 "@stacksjs/env": ["@stacksjs/env@0.70.371", "", {}, "sha512-sXAQJB70wdwHeyFg7Tzvfi2Ox6rxMy04mA90fYED2vXr6bgFETG/7g3CtsO6TARbeGuRxtcfm2XA+9VVUFJWQA=="],
72
7073 "@stacksjs/gitlint": ["@stacksjs/gitlint@0.1.7", "", { "bin": { "gitlint": "./dist/bin/cli.js" } }, "sha512-7d43l2r5gSLXK8YKflvkwZvN/huoJqbUSmDN+OY3CGHjb4ItimV6GJ/8JyFJwGI7tDEc5VgNGL0JYyIbk2IQ6g=="],
7174
7275 "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-wqqAxewpsHVDzpC+IRy+yYyGXGlv1j5GCqYTh8fL7d3LLhnRPN72PEQywM357burtORvdkFHfOzk3Ih1SMvuiw=="],
package.jsonmodified+2-0
Changes to package.json
@@ -43,11 +43,13 @@
4343 "dev:docs": "bun packages/bunpress/bin/cli.ts dev",
4444 "build:docs": "bun packages/bunpress/bin/cli.ts build --outdir ./dist",
4545 "preview:docs": "bun packages/bunpress/bin/cli.ts preview --outdir ./dist",
46 "env:production": "bun scripts/export-production-env.ts",
4647 "typecheck": "bun --bun tsc --noEmit",
4748 "bench": "cd benchmark && bun run bench:all"
4849 },
4950 "devDependencies": {
5051 "@stacksjs/bumpx": "^0.2.4",
52 "@stacksjs/env": "^0.70.371",
5153 "@stacksjs/gitlint": "^0.1.5",
5254 "@stacksjs/logsmith": "^0.2.1",
5355 "bun-git-hooks": "^0.3.1",
scripts/export-production-env.tsadded+48-0
Changes to scripts/export-production-env.ts
@@ -0,0 +1,48 @@
1import { appendFileSync } from 'node:fs'
2import { loadEnv } from '@stacksjs/env/plugin'
3
4const result = loadEnv({
5 path: '.env.production',
6 keysFile: '.env.keys',
7 env: 'production',
8 overload: true,
9 quiet: true,
10})
11
12if (result.errors.length > 0)
13 throw new Error(`Could not load .env.production: ${result.errors.join('; ')}`)
14
15const required = [
16 'AWS_DEFAULT_REGION',
17 'AWS_S3_BUCKET',
18 'AWS_CLOUDFRONT_DISTRIBUTION_ID',
19] as const
20
21const values = Object.fromEntries(required.map((key) => {
22 const value = process.env[key]
23
24 if (!value || value.startsWith('encrypted:'))
25 throw new Error(`Missing decrypted production value: ${key}`)
26
27 if (value.includes('\n') || value.includes('\r'))
28 throw new Error(`Production value must be single-line: ${key}`)
29
30 return [key, value]
31}))
32
33if (!/^[a-z]{2}(?:-gov)?-[a-z]+-\d$/.test(values.AWS_DEFAULT_REGION))
34 throw new Error('AWS_DEFAULT_REGION is invalid')
35
36if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(values.AWS_S3_BUCKET))
37 throw new Error('AWS_S3_BUCKET is invalid')
38
39if (!/^[A-Z0-9]{13,14}$/.test(values.AWS_CLOUDFRONT_DISTRIBUTION_ID))
40 throw new Error('AWS_CLOUDFRONT_DISTRIBUTION_ID is invalid')
41
42const githubEnv = process.env.GITHUB_ENV
43
44if (!githubEnv)
45 throw new Error('GITHUB_ENV is required')
46
47appendFileSync(githubEnv, `${Object.entries(values).map(([key, value]) => `${key}=${value}`).join('\n')}\n`)
48console.log(`Loaded ${required.length} encrypted production values`)