also looking at this
fix(ci): deploy with encrypted production env
#82
6 files
+89
-16
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -0,0 +1,9 @@ | ||
| 1 | #/-------------------[DOTENV_PUBLIC_KEY]--------------------/ | |
| 2 | #/ versioned X25519 encryption for .env files / | |
| 3 | #/ [how it works](https://stacksjs.com/encryption) / | |
| 4 | #/----------------------------------------------------------/ | |
| 5 | DOTENV_PUBLIC_KEY_PRODUCTION="x25519-public:MCowBQYDK2VuAyEAVflLYovF9BhnwsVpiWU85Ch4zzQqqqCP1x2F2s0AuSQ" | |
| 6 | ||
| 7 | AWS_DEFAULT_REGION="encrypted:v2:eyJ2IjoyLCJlcGsiOiJNQ293QlFZREsyVnVBeUVBR1ROZENaNVZHUTZ6MnVDWmR1U3pfRk1ZbldHaEZFamhkbXFTbmQwbEZWSSIsInNhbHQiOiIwaHFPYWtJdlU3ZWlTMElrTXhPMmFRIiwibm9uY2UiOiJEaVowRDBzbDdxSlBzYzRxIiwiY2lwaGVydGV4dCI6Ik1nQ002VGRhcWhKYSIsInRhZyI6InNGVFFETXF4QjFKTzhkVmxzcGZCcFEifQ" | |
| 8 | AWS_S3_BUCKET="encrypted:v2:eyJ2IjoyLCJlcGsiOiJNQ293QlFZREsyVnVBeUVBUDRJblRHa0ZzT212YW9qTHY2N1R1eVNPa3JiYl9rY19LeUFEVy1uS1FIUSIsInNhbHQiOiJacFEzZ251b2dTWFFJUnMyNzV1N1VnIiwibm9uY2UiOiJPTW5OWW1XaEJyMU93THRjIiwiY2lwaGVydGV4dCI6ImRZWWE4YU1uNDVoUC0tMTUiLCJ0YWciOiJUa1Jma0szZ3Y1aTBzNmp1eWFjVnZnIn0" | |
| 9 | AWS_CLOUDFRONT_DISTRIBUTION_ID="encrypted:v2:eyJ2IjoyLCJlcGsiOiJNQ293QlFZREsyVnVBeUVBdXRYc1VLRUZoeXRZUUdlRFpYV1pqdjVBby1nMEY5VUY1c0VDZ1d3bVZVcyIsInNhbHQiOiJ4VDFtN2NCUFFtTEpkcmh2ekFBSTBnIiwibm9uY2UiOiI5UjdGd2JXWFM1ckxES2l4IiwiY2lwaGVydGV4dCI6IlpaN0JjQm9kQXBQWU9SdGwxejgiLCJ0YWciOiJJaTFZSXUzcF9TMkg2Vm9oM00yY0VBIn0" | |
| @@ -112,13 +112,12 @@ jobs: | ||
| 112 | 112 | needs: [lint, typecheck, test] |
| 113 | 113 | if: github.event_name == 'push' && github.ref == 'refs/heads/main' |
| 114 | 114 | runs-on: ubuntu-latest |
| 115 | permissions: | |
| 116 | contents: read | |
| 117 | id-token: write | |
| 115 | 118 | concurrency: |
| 116 | 119 | group: bunpress-org-production |
| 117 | 120 | 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 | |
| 122 | 121 | |
| 123 | 122 | steps: |
| 124 | 123 | - uses: actions/checkout@v6 |
| @@ -133,6 +132,26 @@ jobs: | ||
| 133 | 132 | - name: Install dependencies |
| 134 | 133 | run: bun install --frozen-lockfile |
| 135 | 134 | |
| 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 | ||
| 136 | 155 | - name: Build documentation |
| 137 | 156 | run: bun run build:docs |
| 138 | 157 | |
| @@ -141,19 +160,9 @@ jobs: | ||
| 141 | 160 | run: | |
| 142 | 161 | set -euo pipefail |
| 143 | 162 | |
| 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}" \ | |
| 155 | 164 | --delete \ |
| 156 | 165 | --cache-control 'max-age=3600, public' |
| 157 | 166 | aws cloudfront create-invalidation \ |
| 158 | --distribution-id "$distribution_id" \ | |
| 167 | --distribution-id "$AWS_CLOUDFRONT_DISTRIBUTION_ID" \ | |
| 159 | 168 | --paths '/*' |
| @@ -6,6 +6,7 @@ | ||
| 6 | 6 | "name": "bunpress-monorepo", |
| 7 | 7 | "devDependencies": { |
| 8 | 8 | "@stacksjs/bumpx": "^0.2.4", |
| 9 | "@stacksjs/env": "^0.70.371", | |
| 9 | 10 | "@stacksjs/gitlint": "^0.1.5", |
| 10 | 11 | "@stacksjs/logsmith": "^0.2.1", |
| 11 | 12 | "bun-git-hooks": "^0.3.1", |
| @@ -67,6 +68,8 @@ | ||
| 67 | 68 | |
| 68 | 69 | "@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=="], |
| 69 | 70 | |
| 71 | "@stacksjs/env": ["@stacksjs/env@0.70.371", "", {}, "sha512-sXAQJB70wdwHeyFg7Tzvfi2Ox6rxMy04mA90fYED2vXr6bgFETG/7g3CtsO6TARbeGuRxtcfm2XA+9VVUFJWQA=="], | |
| 72 | ||
| 70 | 73 | "@stacksjs/gitlint": ["@stacksjs/gitlint@0.1.7", "", { "bin": { "gitlint": "./dist/bin/cli.js" } }, "sha512-7d43l2r5gSLXK8YKflvkwZvN/huoJqbUSmDN+OY3CGHjb4ItimV6GJ/8JyFJwGI7tDEc5VgNGL0JYyIbk2IQ6g=="], |
| 71 | 74 | |
| 72 | 75 | "@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=="], |
| @@ -43,11 +43,13 @@ | ||
| 43 | 43 | "dev:docs": "bun packages/bunpress/bin/cli.ts dev", |
| 44 | 44 | "build:docs": "bun packages/bunpress/bin/cli.ts build --outdir ./dist", |
| 45 | 45 | "preview:docs": "bun packages/bunpress/bin/cli.ts preview --outdir ./dist", |
| 46 | "env:production": "bun scripts/export-production-env.ts", | |
| 46 | 47 | "typecheck": "bun --bun tsc --noEmit", |
| 47 | 48 | "bench": "cd benchmark && bun run bench:all" |
| 48 | 49 | }, |
| 49 | 50 | "devDependencies": { |
| 50 | 51 | "@stacksjs/bumpx": "^0.2.4", |
| 52 | "@stacksjs/env": "^0.70.371", | |
| 51 | 53 | "@stacksjs/gitlint": "^0.1.5", |
| 52 | 54 | "@stacksjs/logsmith": "^0.2.1", |
| 53 | 55 | "bun-git-hooks": "^0.3.1", |
| @@ -0,0 +1,48 @@ | ||
| 1 | import { appendFileSync } from 'node:fs' | |
| 2 | import { loadEnv } from '@stacksjs/env/plugin' | |
| 3 | ||
| 4 | const result = loadEnv({ | |
| 5 | path: '.env.production', | |
| 6 | keysFile: '.env.keys', | |
| 7 | env: 'production', | |
| 8 | overload: true, | |
| 9 | quiet: true, | |
| 10 | }) | |
| 11 | ||
| 12 | if (result.errors.length > 0) | |
| 13 | throw new Error(`Could not load .env.production: ${result.errors.join('; ')}`) | |
| 14 | ||
| 15 | const required = [ | |
| 16 | 'AWS_DEFAULT_REGION', | |
| 17 | 'AWS_S3_BUCKET', | |
| 18 | 'AWS_CLOUDFRONT_DISTRIBUTION_ID', | |
| 19 | ] as const | |
| 20 | ||
| 21 | const 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 | ||
| 33 | if (!/^[a-z]{2}(?:-gov)?-[a-z]+-\d$/.test(values.AWS_DEFAULT_REGION)) | |
| 34 | throw new Error('AWS_DEFAULT_REGION is invalid') | |
| 35 | ||
| 36 | if (!/^[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 | ||
| 39 | if (!/^[A-Z0-9]{13,14}$/.test(values.AWS_CLOUDFRONT_DISTRIBUTION_ID)) | |
| 40 | throw new Error('AWS_CLOUDFRONT_DISTRIBUTION_ID is invalid') | |
| 41 | ||
| 42 | const githubEnv = process.env.GITHUB_ENV | |
| 43 | ||
| 44 | if (!githubEnv) | |
| 45 | throw new Error('GITHUB_ENV is required') | |
| 46 | ||
| 47 | appendFileSync(githubEnv, `${Object.entries(values).map(([key, value]) => `${key}=${value}`).join('\n')}\n`) | |
| 48 | console.log(`Loaded ${required.length} encrypted production values`) | |