grada-run 0.0.2 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/.github/workflows/deploy-docs.yml +37 -0
  2. package/.github/workflows/iac-validation.yml +303 -0
  3. package/.github/workflows/publish.yml +68 -0
  4. package/.github/workflows/sync-bedrock-models.yml +57 -0
  5. package/.github/workflows/test.yml +43 -0
  6. package/.muserules +31 -0
  7. package/LICENSE +21 -0
  8. package/README.md +193 -3
  9. package/apps/docs/.astro/collections/docs.schema.json +644 -0
  10. package/apps/docs/.astro/content-assets.mjs +4 -0
  11. package/apps/docs/.astro/content-modules.mjs +4 -0
  12. package/apps/docs/.astro/content.d.ts +179 -0
  13. package/apps/docs/.astro/data-store.json +1 -0
  14. package/apps/docs/.astro/dev.json +14 -0
  15. package/apps/docs/.astro/settings.json +5 -0
  16. package/apps/docs/.astro/types.d.ts +2 -0
  17. package/apps/docs/astro.config.mjs +97 -0
  18. package/apps/docs/package.json +17 -0
  19. package/apps/docs/src/content/docs/adrs/0001-s3-native-state-locking.md +37 -0
  20. package/apps/docs/src/content/docs/adrs/0002-eject-mechanism-pure-iac.md +39 -0
  21. package/apps/docs/src/content/docs/adrs/0003-sync-ai-context-strategy.md +48 -0
  22. package/apps/docs/src/content/docs/adrs/0004-iac-driven-diagnostic-context.md +37 -0
  23. package/apps/docs/src/content/docs/adrs/0005-ecs-fargate-alb-runtime-target.md +38 -0
  24. package/apps/docs/src/content/docs/adrs/0006-github-oidc-no-stored-keys.md +37 -0
  25. package/apps/docs/src/content/docs/adrs/0007-framework-detection-with-fallback.md +37 -0
  26. package/apps/docs/src/content/docs/adrs/0008-secrets-names-in-git-values-in-aws.md +37 -0
  27. package/apps/docs/src/content/docs/adrs/0009-regenerate-with-backup-on-rerun.md +37 -0
  28. package/apps/docs/src/content/docs/adrs/0010-advisory-only-security-scans.md +37 -0
  29. package/apps/docs/src/content/docs/cli/add.md +84 -0
  30. package/apps/docs/src/content/docs/cli/apply.md +32 -0
  31. package/apps/docs/src/content/docs/cli/db.md +200 -0
  32. package/apps/docs/src/content/docs/cli/destroy.md +31 -0
  33. package/apps/docs/src/content/docs/cli/diagnose.md +37 -0
  34. package/apps/docs/src/content/docs/cli/doctor.md +28 -0
  35. package/apps/docs/src/content/docs/cli/domain.md +57 -0
  36. package/apps/docs/src/content/docs/cli/drift.md +40 -0
  37. package/apps/docs/src/content/docs/cli/eject.md +29 -0
  38. package/apps/docs/src/content/docs/cli/exec.md +49 -0
  39. package/apps/docs/src/content/docs/cli/gc.md +37 -0
  40. package/apps/docs/src/content/docs/cli/init.md +72 -0
  41. package/apps/docs/src/content/docs/cli/logs.md +39 -0
  42. package/apps/docs/src/content/docs/cli/rollback.md +51 -0
  43. package/apps/docs/src/content/docs/cli/secrets.md +73 -0
  44. package/apps/docs/src/content/docs/cli/sleep.md +53 -0
  45. package/apps/docs/src/content/docs/cli/status.md +34 -0
  46. package/apps/docs/src/content/docs/cli/sync-ai.md +27 -0
  47. package/apps/docs/src/content/docs/guides/architecture.md +87 -0
  48. package/apps/docs/src/content/docs/guides/aws-credentials.md +72 -0
  49. package/apps/docs/src/content/docs/guides/background-workers.md +45 -0
  50. package/apps/docs/src/content/docs/guides/cicd-pipeline.md +64 -0
  51. package/apps/docs/src/content/docs/guides/database-connections.md +64 -0
  52. package/apps/docs/src/content/docs/guides/docker-compose.md +37 -0
  53. package/apps/docs/src/content/docs/guides/dockerfiles.md +46 -0
  54. package/apps/docs/src/content/docs/guides/ephemeral-pr-previews.md +39 -0
  55. package/apps/docs/src/content/docs/guides/examples.md +50 -0
  56. package/apps/docs/src/content/docs/guides/frameworks.md +88 -0
  57. package/apps/docs/src/content/docs/guides/headless.md +75 -0
  58. package/apps/docs/src/content/docs/guides/quickstart.md +52 -0
  59. package/apps/docs/src/content/docs/guides/rerun-init.md +43 -0
  60. package/apps/docs/src/content/docs/guides/secrets-management.md +83 -0
  61. package/apps/docs/src/content/docs/guides/understanding-your-bill.md +63 -0
  62. package/apps/docs/src/content/docs/index.mdx +103 -0
  63. package/apps/docs/src/content/docs/migrations/astro-vercel-to-aws.md +55 -0
  64. package/apps/docs/src/content/docs/migrations/heroku-procfile-to-aws.md +41 -0
  65. package/apps/docs/src/content/docs/migrations/nextjs-vercel-to-aws.md +51 -0
  66. package/apps/docs/src/content/docs/migrations/sveltekit-vercel-to-aws.md +63 -0
  67. package/apps/docs/src/content/docs/roadmap.md +97 -0
  68. package/apps/docs/src/content/docs/testing-strategy.md +32 -0
  69. package/apps/docs/src/content.config.ts +7 -0
  70. package/apps/docs/src/custom.css +14 -0
  71. package/apps/docs/tsconfig.json +6 -0
  72. package/bin/cli.js +140 -0
  73. package/package.json +105 -7
  74. package/scripts/sync-bedrock-models.js +22 -0
  75. package/scripts/test-iac.js +261 -0
  76. package/specs/add-redis-sqs-bedrock.md +128 -0
  77. package/specs/add-storage-dynamodb.md +106 -0
  78. package/specs/bedrock-model-catalog.md +131 -0
  79. package/specs/ci-pipeline.md +17 -0
  80. package/specs/cost-transparency.md +115 -0
  81. package/specs/custom-domains-and-ses.md +153 -0
  82. package/specs/database-suite-expansion.md +151 -0
  83. package/specs/db-connect.md +69 -0
  84. package/specs/db-lifecycle-migrations.md +159 -0
  85. package/specs/dependency-aware-init.md +176 -0
  86. package/specs/deploy-stack-to-grada-run-rebrand.md +45 -0
  87. package/specs/deployment-safety.md +170 -0
  88. package/specs/diagnose.md +16 -0
  89. package/specs/docs-hub.md +16 -0
  90. package/specs/dx-polish.md +46 -0
  91. package/specs/exec.md +25 -0
  92. package/specs/finops-cron-drift.md +161 -0
  93. package/specs/gc.md +26 -0
  94. package/specs/integration-suite.md +16 -0
  95. package/specs/logs.md +32 -0
  96. package/specs/rollback-live-polling.md +40 -0
  97. package/specs/secrets-pull-audit.md +51 -0
  98. package/specs/serverless-lambda-target.md +133 -0
  99. package/specs/status.md +31 -0
  100. package/specs/telemetry-and-spawn-hardening.md +69 -0
  101. package/specs/telemetry-hardening.md +35 -0
  102. package/src/commands/add.js +1111 -0
  103. package/src/commands/apply.js +214 -0
  104. package/src/commands/db/backup.js +229 -0
  105. package/src/commands/db/connect.js +304 -0
  106. package/src/commands/db/enable-vector.js +344 -0
  107. package/src/commands/db/import.js +604 -0
  108. package/src/commands/db/migrate.js +477 -0
  109. package/src/commands/db/restore.js +361 -0
  110. package/src/commands/db.js +87 -0
  111. package/src/commands/destroy.js +217 -0
  112. package/src/commands/diagnose.js +460 -0
  113. package/src/commands/doctor.js +109 -0
  114. package/src/commands/domain.js +685 -0
  115. package/src/commands/drift.js +243 -0
  116. package/src/commands/eject.js +127 -0
  117. package/src/commands/exec.js +222 -0
  118. package/src/commands/gc.js +250 -0
  119. package/src/commands/init.js +649 -0
  120. package/src/commands/logs.js +256 -0
  121. package/src/commands/rollback.js +323 -0
  122. package/src/commands/secrets.js +485 -0
  123. package/src/commands/sleep.js +347 -0
  124. package/src/commands/status.js +309 -0
  125. package/src/commands/sync-ai.js +115 -0
  126. package/src/commands/wake.js +337 -0
  127. package/src/core/parser.js +126 -0
  128. package/src/core/telemetry.js +244 -0
  129. package/src/data/bedrock-models.json +896 -0
  130. package/src/utils/addons.js +126 -0
  131. package/src/utils/ai-rules.js +59 -0
  132. package/src/utils/args.js +91 -0
  133. package/src/utils/aws.js +178 -0
  134. package/src/utils/backup.js +69 -0
  135. package/src/utils/bedrock-catalog.js +511 -0
  136. package/src/utils/capabilities.js +500 -0
  137. package/src/utils/command.js +65 -0
  138. package/src/utils/db-tunnel.js +164 -0
  139. package/src/utils/detector.js +298 -0
  140. package/src/utils/dockerCompose.js +65 -0
  141. package/src/utils/domains.js +73 -0
  142. package/src/utils/ecs-runner.js +289 -0
  143. package/src/utils/ecs.js +92 -0
  144. package/src/utils/frameworks.js +55 -0
  145. package/src/utils/generator.js +527 -0
  146. package/src/utils/hcl.js +426 -0
  147. package/src/utils/lambda-ecr.js +185 -0
  148. package/src/utils/prompts.js +278 -0
  149. package/src/utils/rds.js +131 -0
  150. package/src/utils/resolvers.js +174 -0
  151. package/src/utils/sleep-state.js +140 -0
  152. package/src/utils/sleep-targets.js +139 -0
  153. package/src/utils/system.js +42 -0
  154. package/src/utils/terraform.js +70 -0
  155. package/src/utils/visualizer.js +381 -0
  156. package/src/utils/warnings.js +49 -0
  157. package/templates/README.md +150 -0
  158. package/templates/docker/django.Dockerfile +40 -0
  159. package/templates/docker/go.Dockerfile +23 -0
  160. package/templates/docker/nestjs.Dockerfile +33 -0
  161. package/templates/docker/nextjs.Dockerfile +55 -0
  162. package/templates/docker/node.Dockerfile +24 -0
  163. package/templates/docker/nuxt.Dockerfile +47 -0
  164. package/templates/docker/python.Dockerfile +38 -0
  165. package/templates/docker/rails.Dockerfile +59 -0
  166. package/templates/docker/static.Dockerfile +32 -0
  167. package/templates/docker/svelte.Dockerfile +52 -0
  168. package/templates/github/deploy-lambda.yml +120 -0
  169. package/templates/github/deploy.yml +138 -0
  170. package/templates/github/drift.yml +112 -0
  171. package/templates/github/preview-lambda.yml +86 -0
  172. package/templates/github/preview.yml +69 -0
  173. package/templates/github/teardown.yml +43 -0
  174. package/templates/terraform/addons/bedrock.tf +34 -0
  175. package/templates/terraform/addons/cron-lambda.tf +78 -0
  176. package/templates/terraform/addons/cron.tf +101 -0
  177. package/templates/terraform/addons/dynamodb.tf +73 -0
  178. package/templates/terraform/addons/redis.tf +64 -0
  179. package/templates/terraform/addons/s3.tf +143 -0
  180. package/templates/terraform/addons/ses.tf +73 -0
  181. package/templates/terraform/addons/sqs.tf +67 -0
  182. package/templates/terraform/backend.tf +22 -0
  183. package/templates/terraform/cloudfront-lambda.tf +80 -0
  184. package/templates/terraform/cloudfront.tf +80 -0
  185. package/templates/terraform/database-aurora-postgresql.tf +92 -0
  186. package/templates/terraform/database-mysql.tf +72 -0
  187. package/templates/terraform/database.tf +71 -0
  188. package/templates/terraform/main-lambda.tf +229 -0
  189. package/templates/terraform/main.tf +296 -0
  190. package/templates/terraform/network.tf +95 -0
  191. package/templates/terraform/oidc.tf +64 -0
  192. package/templates/terraform/secrets.tf +31 -0
  193. package/templates/terraform/worker.tf +69 -0
  194. package/tests/__snapshots__/generator.test.js.snap +9633 -0
  195. package/tests/add.test.js +2037 -0
  196. package/tests/ai.test.js +94 -0
  197. package/tests/apply.test.js +488 -0
  198. package/tests/args.test.js +86 -0
  199. package/tests/aws.test.js +244 -0
  200. package/tests/capabilities.test.js +307 -0
  201. package/tests/cli.test.js +29 -0
  202. package/tests/command.test.js +100 -0
  203. package/tests/commands-import.test.js +74 -0
  204. package/tests/db.test.js +2704 -0
  205. package/tests/destroy.test.js +391 -0
  206. package/tests/detector.test.js +79 -0
  207. package/tests/diagnose.test.js +779 -0
  208. package/tests/doctor.test.js +202 -0
  209. package/tests/domain.test.js +899 -0
  210. package/tests/drift.test.js +243 -0
  211. package/tests/ecs.test.js +130 -0
  212. package/tests/eject.test.js +65 -0
  213. package/tests/exec.test.js +380 -0
  214. package/tests/gc.test.js +496 -0
  215. package/tests/generator.test.js +794 -0
  216. package/tests/headless.test.js +562 -0
  217. package/tests/lambda-ecr.test.js +185 -0
  218. package/tests/logs.test.js +447 -0
  219. package/tests/parser.test.js +160 -0
  220. package/tests/rds.test.js +244 -0
  221. package/tests/resolvers.test.js +282 -0
  222. package/tests/rollback.test.js +692 -0
  223. package/tests/secrets.test.js +752 -0
  224. package/tests/sleep-wake.test.js +1016 -0
  225. package/tests/status.test.js +370 -0
  226. package/tests/system.test.js +70 -0
  227. package/tests/telemetry.test.js +520 -0
  228. package/tests/terraform.test.js +84 -0
  229. package/tests/visualizer.test.js +496 -0
  230. package/vitest.config.js +9 -0
  231. package/index.js +0 -2
@@ -0,0 +1,112 @@
1
+ name: IaC Drift Detection
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '0 6 * * *'
6
+ workflow_dispatch:
7
+
8
+ env:
9
+ AWS_REGION: {{REGION}}
10
+
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+ issues: write
15
+
16
+ jobs:
17
+ drift:
18
+ name: Detect Terraform drift
19
+ runs-on: ubuntu-latest
20
+
21
+ steps:
22
+ - name: Checkout Code
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Configure AWS Credentials
26
+ uses: aws-actions/configure-aws-credentials@v4
27
+ with:
28
+ role-to-assume: {{ROLE_ARN}}
29
+ aws-region: ${{ env.AWS_REGION }}
30
+
31
+ - name: Setup Terraform
32
+ uses: hashicorp/setup-terraform@v3
33
+ with:
34
+ # The wrapper mangles -detailed-exitcode; raw codes (0/1/2) are the signal.
35
+ terraform_wrapper: false
36
+
37
+ - name: Terraform Init
38
+ working-directory: ./terraform
39
+ run: terraform init -input=false
40
+
41
+ # -detailed-exitcode: 0 = no changes, 1 = error, 2 = drift detected.
42
+ - name: Terraform Plan
43
+ id: plan
44
+ working-directory: ./terraform
45
+ run: |
46
+ set +e
47
+ terraform plan -detailed-exitcode -no-color -out=tfplan > plan-full.txt 2>&1
48
+ EXITCODE=$?
49
+ echo "exitcode=$EXITCODE" >> "$GITHUB_OUTPUT"
50
+ if [ "$EXITCODE" != "1" ]; then
51
+ terraform show -no-color tfplan > plan.txt
52
+ else
53
+ cp plan-full.txt plan.txt
54
+ fi
55
+ echo "### Drift plan (exit $EXITCODE)" >> "$GITHUB_STEP_SUMMARY"
56
+ head -50 plan.txt >> "$GITHUB_STEP_SUMMARY"
57
+
58
+ - name: Report drift as GitHub Issue
59
+ if: steps.plan.outputs.exitcode == '2' || steps.plan.outputs.exitcode == '0'
60
+ uses: actions/github-script@v7
61
+ with:
62
+ script: |
63
+ const fs = require('fs');
64
+ const { owner, repo } = context.repo;
65
+ const exitcode = '${{ steps.plan.outputs.exitcode }}';
66
+ const label = 'iac-drift';
67
+ const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`;
68
+ let diff = '';
69
+ try {
70
+ diff = fs.readFileSync('terraform/plan.txt', 'utf8');
71
+ } catch (error) {
72
+ diff = '(plan output unavailable)';
73
+ }
74
+ if (diff.length > 30000) {
75
+ diff = `${diff.slice(0, 30000)}\n... (truncated, see ${runUrl})`;
76
+ }
77
+ try {
78
+ await github.rest.issues.getLabel({ owner, repo, name: label });
79
+ } catch (error) {
80
+ if (error.status === 404) {
81
+ await github.rest.issues.createLabel({ owner, repo, name: label, color: 'd93f0b', description: 'Out-of-band AWS infrastructure changes detected by Terraform' });
82
+ } else {
83
+ throw error;
84
+ }
85
+ }
86
+ const { data: open } = await github.rest.issues.listForRepo({ owner, repo, state: 'open', labels: label });
87
+ const existing = open[0];
88
+ if (exitcode === '2') {
89
+ const body = `## ⚠️ Infrastructure Drift Detected in AWS\n\nLive AWS state differs from Terraform on \`${new Date().toISOString()}\` ([workflow run](${runUrl})).\n\n\`\`\`terraform\n${diff}\n\`\`\`\n\nReconcile with \`npx grada-run apply\`, or adopt the live resource with \`terraform import\`.`;
90
+ if (existing) {
91
+ await github.rest.issues.createComment({ owner, repo, issue_number: existing.number, body: `Drift still present on \`${new Date().toISOString()}\` ([run](${runUrl})).\n\n\`\`\`terraform\n${diff}\n\`\`\`` });
92
+ await github.rest.issues.update({ owner, repo, issue_number: existing.number, body });
93
+ } else {
94
+ await github.rest.issues.create({ owner, repo, title: '⚠️ Infrastructure Drift Detected in AWS', labels: [label], body });
95
+ }
96
+ } else if (existing) {
97
+ await github.rest.issues.createComment({ owner, repo, issue_number: existing.number, body: `✅ Drift resolved on \`${new Date().toISOString()}\` ([run](${runUrl})).` });
98
+ await github.rest.issues.update({ owner, repo, issue_number: existing.number, state: 'closed' });
99
+ }
100
+
101
+ - name: Notify Slack
102
+ if: steps.plan.outputs.exitcode == '2' && secrets.SLACK_WEBHOOK_URL != ''
103
+ env:
104
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
105
+ run: |
106
+ SUMMARY=$(head -c 1500 terraform/plan.txt)
107
+ jq -n \
108
+ --arg repo "$GITHUB_REPOSITORY" \
109
+ --arg url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
110
+ --arg summary "$SUMMARY" \
111
+ '{text: (":warning: Infrastructure drift detected in " + $repo + "\n" + $url + "\n```" + $summary + "```")}' > /tmp/drift-slack.json
112
+ curl -sS -X POST -H 'Content-Type: application/json' -d @/tmp/drift-slack.json "$SLACK_WEBHOOK_URL"
@@ -0,0 +1,86 @@
1
+ name: Ephemeral PR Preview
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ env:
8
+ AWS_REGION: {{REGION}}
9
+ ECR_REPOSITORY: {{PROJECT_NAME}}-repo
10
+ WORKSPACE: pr-${{ github.event.pull_request.number }}
11
+
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+ pull-requests: write # Required to post the comment
16
+
17
+ jobs:
18
+ deploy-preview:
19
+ name: Provision Preview Environment
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: Checkout Code
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Configure AWS credentials
27
+ uses: aws-actions/configure-aws-credentials@v4
28
+ with:
29
+ role-to-assume: arn:aws:iam::{{AWS_ACCOUNT_ID}}:role/{{PROJECT_NAME}}-github-actions-role
30
+ aws-region: ${{ env.AWS_REGION }}
31
+
32
+ - name: Login to Amazon ECR
33
+ id: login-ecr
34
+ uses: aws-actions/amazon-ecr-login@v2
35
+
36
+ - name: Build and Push Docker image
37
+ env:
38
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
39
+ run: |
40
+ # We tag the image specifically for this PR workspace
41
+ docker build --platform linux/amd64 -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }} .
42
+ docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }}
43
+
44
+ - name: Setup Terraform
45
+ uses: hashicorp/setup-terraform@v3
46
+
47
+ - name: Terraform Init & Apply
48
+ id: tf-apply
49
+ working-directory: ./terraform
50
+ run: |
51
+ terraform init
52
+ # Create or select the workspace specific to this PR
53
+ terraform workspace select -or-create ${{ env.WORKSPACE }}
54
+ terraform apply -auto-approve
55
+
56
+ - name: Deploy PR image to the preview function
57
+ env:
58
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
59
+ run: |
60
+ # Terraform provisions the function pointing at :latest (which it
61
+ # then ignores), so the PR-tagged image is deployed explicitly —
62
+ # mirroring the workspace-tagged task definition on ECS targets.
63
+ aws lambda update-function-code \
64
+ --function-name {{PROJECT_NAME}}-${{ env.WORKSPACE }}-fn \
65
+ --image-uri $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }} \
66
+ --region ${{ env.AWS_REGION }}
67
+ aws lambda wait function-updated \
68
+ --function-name {{PROJECT_NAME}}-${{ env.WORKSPACE }}-fn \
69
+ --region ${{ env.AWS_REGION }}
70
+
71
+ - name: Read preview URL
72
+ working-directory: ./terraform
73
+ run: |
74
+ API_URL=$(terraform output -raw api_gateway_url)
75
+ echo "PREVIEW_URL=$API_URL" >> $GITHUB_ENV
76
+
77
+ - name: Comment PR
78
+ uses: actions/github-script@v7
79
+ with:
80
+ script: |
81
+ github.rest.issues.createComment({
82
+ issue_number: context.issue.number,
83
+ owner: context.repo.owner,
84
+ repo: context.repo.repo,
85
+ body: `🚀 **Preview Environment Deployed!**\n\nYour ephemeral AWS Lambda environment is live at:\n[${process.env.PREVIEW_URL}](${process.env.PREVIEW_URL})\n\n_This environment will be automatically destroyed when the PR is merged or closed._\n\n---\n<div align="right"><sup>☁️ Generated by <a href="https://github.com/grada-run/grada">Grada</a></sup></div>`
86
+ })
@@ -0,0 +1,69 @@
1
+ name: Ephemeral PR Preview
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ env:
8
+ AWS_REGION: {{REGION}}
9
+ ECR_REPOSITORY: {{PROJECT_NAME}}-repo
10
+ WORKSPACE: pr-${{ github.event.pull_request.number }}
11
+
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+ pull-requests: write # Required to post the comment
16
+
17
+ jobs:
18
+ deploy-preview:
19
+ name: Provision Preview Environment
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: Checkout Code
24
+ uses: actions/checkout@v4
25
+
26
+ - name: Configure AWS credentials
27
+ uses: aws-actions/configure-aws-credentials@v4
28
+ with:
29
+ role-to-assume: arn:aws:iam::{{AWS_ACCOUNT_ID}}:role/{{PROJECT_NAME}}-github-actions-role
30
+ aws-region: ${{ env.AWS_REGION }}
31
+
32
+ - name: Login to Amazon ECR
33
+ id: login-ecr
34
+ uses: aws-actions/amazon-ecr-login@v2
35
+
36
+ - name: Build and Push Docker image
37
+ env:
38
+ ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
39
+ run: |
40
+ # We tag the image specifically for this PR workspace
41
+ docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }} .
42
+ docker push $ECR_REGISTRY/$ECR_REPOSITORY:${{ env.WORKSPACE }}
43
+
44
+ - name: Setup Terraform
45
+ uses: hashicorp/setup-terraform@v3
46
+
47
+ - name: Terraform Init & Apply
48
+ id: tf-apply
49
+ working-directory: ./terraform
50
+ run: |
51
+ terraform init
52
+ # Create or select the workspace specific to this PR
53
+ terraform workspace select -or-create ${{ env.WORKSPACE }}
54
+ terraform apply -auto-approve
55
+
56
+ # Extract the ALB URL to post to GitHub
57
+ ALB_URL=$(terraform output -raw alb_direct_url)
58
+ echo "PREVIEW_URL=$ALB_URL" >> $GITHUB_ENV
59
+
60
+ - name: Comment PR
61
+ uses: actions/github-script@v7
62
+ with:
63
+ script: |
64
+ github.rest.issues.createComment({
65
+ issue_number: context.issue.number,
66
+ owner: context.repo.owner,
67
+ repo: context.repo.repo,
68
+ body: `🚀 **Preview Environment Deployed!**\n\nYour ephemeral AWS Fargate environment is live at:\n[${process.env.PREVIEW_URL}](${process.env.PREVIEW_URL})\n\n_This environment will be automatically destroyed when the PR is merged or closed._\n\n---\n<div align="right"><sup>☁️ Generated by <a href="https://github.com/grada-run/grada">Grada</a></sup></div>`
69
+ })
@@ -0,0 +1,43 @@
1
+ name: Destroy PR Preview
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ env:
8
+ AWS_REGION: {{REGION}}
9
+ WORKSPACE: pr-${{ github.event.pull_request.number }}
10
+
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ jobs:
16
+ destroy-preview:
17
+ name: Teardown Preview Environment
18
+ runs-on: ubuntu-latest
19
+
20
+ steps:
21
+ - name: Checkout Code
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Configure AWS credentials
25
+ uses: aws-actions/configure-aws-credentials@v4
26
+ with:
27
+ role-to-assume: arn:aws:iam::{{AWS_ACCOUNT_ID}}:role/{{PROJECT_NAME}}-github-actions-role
28
+ aws-region: ${{ env.AWS_REGION }}
29
+
30
+ - name: Setup Terraform
31
+ uses: hashicorp/setup-terraform@v3
32
+
33
+ - name: Terraform Init & Destroy
34
+ working-directory: ./terraform
35
+ run: |
36
+ terraform init
37
+ # Select the PR workspace
38
+ terraform workspace select ${{ env.WORKSPACE }}
39
+ # Destroy all resources in this workspace safely
40
+ terraform destroy -auto-approve
41
+ # Switch back to default and delete the PR workspace
42
+ terraform workspace select default
43
+ terraform workspace delete ${{ env.WORKSPACE }}
@@ -0,0 +1,34 @@
1
+ # grada addon: ai:bedrock
2
+ # Generated by `grada add ai:bedrock`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+ #
5
+ # Billing: no fixed hourly instance cost (usage-billed per 1K input/output
6
+ # tokens on InvokeModel calls). Remember to enable model access for your
7
+ # chosen model in the AWS Bedrock console first.
8
+
9
+ # --- Least-Privilege IAM Policy for the ECS Task Role ---
10
+ resource "aws_iam_role_policy" "ai_bedrock_access" {
11
+ name = "${local.app_name}-ai-bedrock-access"
12
+ role = aws_iam_role.task_role.id
13
+ policy = jsonencode({
14
+ Version = "2012-10-17"
15
+ Statement = [
16
+ {
17
+ Effect = "Allow"
18
+ Action = [
19
+ "bedrock:InvokeModel",
20
+ "bedrock:InvokeModelWithResponseStream"
21
+ ]
22
+ Resource = [
23
+ "arn:aws:bedrock:*::foundation-model/*",
24
+ "arn:aws:bedrock:*:*:inference-profile/*"
25
+ ]
26
+ }
27
+ ]
28
+ })
29
+ }
30
+
31
+ # --- Outputs ---
32
+ output "bedrock_model_id" {
33
+ value = "{{BEDROCK_MODEL_ID}}"
34
+ }
@@ -0,0 +1,78 @@
1
+ # grada addon: cron (Lambda target)
2
+ # Generated by `grada add cron` on a --target lambda project. Do not
3
+ # edit by hand unless you know what you are doing — re-running with --force
4
+ # overwrites this file.
5
+ #
6
+ # Billing: $0/mo fixed baseline (first 14M EventBridge Scheduler
7
+ # invocations/mo free); billed only for Lambda invocations while the job runs.
8
+ #
9
+ # NOTE: unlike the ECS variant, the scheduler invokes the web function
10
+ # directly instead of running a container command. Handle the scheduled event
11
+ # in application code (the payload below carries the configured command), or
12
+ # point the schedule at a dedicated function.
13
+
14
+ # --- IAM Role assumed by EventBridge Scheduler ---
15
+ resource "aws_iam_role" "scheduler_cron_role" {
16
+ name = "${local.app_name}-cron-scheduler-role"
17
+ assume_role_policy = jsonencode({
18
+ Version = "2012-10-17"
19
+ Statement = [
20
+ {
21
+ Effect = "Allow"
22
+ Principal = {
23
+ Service = "scheduler.amazonaws.com"
24
+ }
25
+ Action = "sts:AssumeRole"
26
+ }
27
+ ]
28
+ })
29
+ }
30
+
31
+ # --- Least-privilege permission to invoke the function ---
32
+ resource "aws_iam_role_policy" "scheduler_cron_policy" {
33
+ name = "${local.app_name}-cron-scheduler-policy"
34
+ role = aws_iam_role.scheduler_cron_role.id
35
+ policy = jsonencode({
36
+ Version = "2012-10-17"
37
+ Statement = [
38
+ {
39
+ Effect = "Allow"
40
+ Action = ["lambda:InvokeFunction"]
41
+ Resource = [
42
+ aws_lambda_function.app.arn,
43
+ "${aws_lambda_function.app.arn}:*"
44
+ ]
45
+ }
46
+ ]
47
+ })
48
+ }
49
+
50
+ # --- Scheduled invocation (single schedule per project) ---
51
+ resource "aws_scheduler_schedule" "cron" {
52
+ name = "${local.app_name}-cron-{{CRON_NAME}}"
53
+ schedule_expression = "{{SCHEDULE_EXPRESSION}}"
54
+ schedule_expression_timezone = "{{SCHEDULE_TIMEZONE}}"
55
+
56
+ flexible_time_window {
57
+ mode = "OFF"
58
+ }
59
+
60
+ target {
61
+ arn = aws_lambda_function.app.arn
62
+ role_arn = aws_iam_role.scheduler_cron_role.arn
63
+
64
+ input = jsonencode({
65
+ source = "grada-cron"
66
+ command = {{CRON_COMMAND_JSON}}
67
+ })
68
+ }
69
+ }
70
+
71
+ # --- Outputs ---
72
+ output "cron_schedule_name" {
73
+ value = aws_scheduler_schedule.cron.name
74
+ }
75
+
76
+ output "cron_schedule_arn" {
77
+ value = aws_scheduler_schedule.cron.arn
78
+ }
@@ -0,0 +1,101 @@
1
+ # grada addon: cron
2
+ # Generated by `grada add cron`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+ #
5
+ # Billing: $0/mo fixed baseline (first 14M EventBridge Scheduler
6
+ # invocations/mo free); billed only for Fargate seconds while the task runs.
7
+
8
+ # --- IAM Role assumed by EventBridge Scheduler ---
9
+ resource "aws_iam_role" "scheduler_cron_role" {
10
+ name = "${local.app_name}-cron-scheduler-role"
11
+ assume_role_policy = jsonencode({
12
+ Version = "2012-10-17"
13
+ Statement = [
14
+ {
15
+ Effect = "Allow"
16
+ Principal = {
17
+ Service = "scheduler.amazonaws.com"
18
+ }
19
+ Action = "sts:AssumeRole"
20
+ }
21
+ ]
22
+ })
23
+ }
24
+
25
+ # --- Least-privilege permission to launch the scheduled task ---
26
+ resource "aws_iam_role_policy" "scheduler_cron_policy" {
27
+ name = "${local.app_name}-cron-scheduler-policy"
28
+ role = aws_iam_role.scheduler_cron_role.id
29
+ policy = jsonencode({
30
+ Version = "2012-10-17"
31
+ Statement = [
32
+ {
33
+ Effect = "Allow"
34
+ Action = ["ecs:RunTask"]
35
+ Resource = [
36
+ aws_ecs_task_definition.app.arn,
37
+ "${aws_ecs_task_definition.app.arn_without_revision}:*"
38
+ ]
39
+ },
40
+ {
41
+ Effect = "Allow"
42
+ Action = ["iam:PassRole"]
43
+ Resource = [
44
+ aws_iam_role.execution_role.arn,
45
+ aws_iam_role.task_role.arn
46
+ ]
47
+ Condition = {
48
+ StringLike = {
49
+ "iam:PassedToService" = "ecs-tasks.amazonaws.com"
50
+ }
51
+ }
52
+ }
53
+ ]
54
+ })
55
+ }
56
+
57
+ # --- Scheduled task (single schedule per project) ---
58
+ resource "aws_scheduler_schedule" "cron" {
59
+ name = "${local.app_name}-cron-{{CRON_NAME}}"
60
+ schedule_expression = "{{SCHEDULE_EXPRESSION}}"
61
+ schedule_expression_timezone = "{{SCHEDULE_TIMEZONE}}"
62
+
63
+ flexible_time_window {
64
+ mode = "OFF"
65
+ }
66
+
67
+ target {
68
+ arn = aws_ecs_cluster.main.arn
69
+ role_arn = aws_iam_role.scheduler_cron_role.arn
70
+
71
+ ecs_parameters {
72
+ task_definition_arn = aws_ecs_task_definition.app.arn
73
+ launch_type = "FARGATE"
74
+ task_count = 1
75
+
76
+ network_configuration {
77
+ subnets = aws_subnet.public[*].id
78
+ security_groups = [aws_security_group.ecs_tasks.id]
79
+ assign_public_ip = true
80
+ }
81
+ }
82
+
83
+ input = jsonencode({
84
+ containerOverrides = [
85
+ {
86
+ name = "${local.app_name}-container"
87
+ command = ["sh", "-c", {{CRON_COMMAND_JSON}}]
88
+ }
89
+ ]
90
+ })
91
+ }
92
+ }
93
+
94
+ # --- Outputs ---
95
+ output "cron_schedule_name" {
96
+ value = aws_scheduler_schedule.cron.name
97
+ }
98
+
99
+ output "cron_schedule_arn" {
100
+ value = aws_scheduler_schedule.cron.arn
101
+ }
@@ -0,0 +1,73 @@
1
+ # grada addon: db:dynamodb
2
+ # Generated by `grada add db:dynamodb`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+
5
+ # --- Scale-to-Zero DynamoDB Table ---
6
+ resource "aws_dynamodb_table" "main" {
7
+ name = "${local.app_name}-table"
8
+ billing_mode = "PAY_PER_REQUEST"
9
+ hash_key = "{{PARTITION_KEY}}"
10
+
11
+ attribute {
12
+ name = "{{PARTITION_KEY}}"
13
+ type = "S"
14
+ }
15
+
16
+ point_in_time_recovery {
17
+ enabled = true
18
+ }
19
+
20
+ server_side_encryption {
21
+ enabled = true
22
+ }
23
+ }
24
+
25
+ # --- VPC Gateway Endpoint (keeps traffic on the AWS network) ---
26
+ # The endpoint itself carries no hourly or data-processing charge. The table
27
+ # above has no fixed hourly instance cost (usage-billed): table storage
28
+ # ($0.25/GB-mo), read/write requests, and PITR continuous backups
29
+ # ($0.20/GB-mo once data is written) are billed by usage.
30
+ resource "aws_vpc_endpoint" "dynamodb" {
31
+ vpc_id = aws_vpc.main.id
32
+ service_name = "com.amazonaws.{{REGION}}.dynamodb"
33
+ vpc_endpoint_type = "Gateway"
34
+ route_table_ids = [aws_route_table.public.id]
35
+ }
36
+
37
+ # --- Least-Privilege IAM Policy for the ECS Task Role ---
38
+ resource "aws_iam_role_policy" "dynamodb_access" {
39
+ name = "${local.app_name}-dynamodb-access"
40
+ role = aws_iam_role.task_role.id
41
+ policy = jsonencode({
42
+ Version = "2012-10-17"
43
+ Statement = [
44
+ {
45
+ Effect = "Allow"
46
+ Action = [
47
+ "dynamodb:GetItem",
48
+ "dynamodb:PutItem",
49
+ "dynamodb:UpdateItem",
50
+ "dynamodb:DeleteItem",
51
+ "dynamodb:Query",
52
+ "dynamodb:Scan",
53
+ "dynamodb:BatchGetItem",
54
+ "dynamodb:BatchWriteItem",
55
+ "dynamodb:DescribeTable"
56
+ ]
57
+ Resource = [
58
+ aws_dynamodb_table.main.arn,
59
+ "${aws_dynamodb_table.main.arn}/index/*"
60
+ ]
61
+ }
62
+ ]
63
+ })
64
+ }
65
+
66
+ # --- Outputs ---
67
+ output "dynamodb_table_name" {
68
+ value = aws_dynamodb_table.main.name
69
+ }
70
+
71
+ output "dynamodb_table_arn" {
72
+ value = aws_dynamodb_table.main.arn
73
+ }
@@ -0,0 +1,64 @@
1
+ # grada addon: db:redis
2
+ # Generated by `grada add db:redis`. Do not edit by hand unless
3
+ # you know what you are doing — re-running with --force overwrites this file.
4
+ #
5
+ # Billing: fixed ~$9.49/mo baseline (cache.t4g.micro Valkey 8.0 at $0.013/hr
6
+ # in us-east-2); $0 intra-AZ VPC data transfer.
7
+
8
+ # --- ElastiCache Subnet Group ---
9
+ resource "aws_elasticache_subnet_group" "redis" {
10
+ name = "${local.app_name}-redis-subnets"
11
+ subnet_ids = aws_subnet.public[*].id
12
+ }
13
+
14
+ # --- Redis Security Group (ECS tasks only) ---
15
+ resource "aws_security_group" "redis" {
16
+ name = "${local.app_name}-redis-sg"
17
+ description = "Allow Redis/Valkey traffic strictly from ECS tasks"
18
+ vpc_id = aws_vpc.main.id
19
+
20
+ ingress {
21
+ from_port = 6379
22
+ to_port = 6379
23
+ protocol = "tcp"
24
+ security_groups = [aws_security_group.ecs_tasks.id]
25
+ }
26
+
27
+ egress {
28
+ from_port = 0
29
+ to_port = 0
30
+ protocol = "-1"
31
+ # trivy:ignore:AVD-AWS-0104 - Allow cache nodes to reach standard AWS services
32
+ cidr_blocks = ["0.0.0.0/0"]
33
+ }
34
+ }
35
+
36
+ # --- ElastiCache for Valkey Replication Group ---
37
+ # Valkey requires aws_elasticache_replication_group even for a single node:
38
+ # the aws_elasticache_cluster API rejects engine = "valkey".
39
+ resource "aws_elasticache_replication_group" "redis" {
40
+ # replication_group_id allows at most 40 chars and must start with a letter.
41
+ # The ds- prefix guarantees a leading letter and the md5 suffix keeps
42
+ # default and PR-preview workspaces unique.
43
+ replication_group_id = "ds-${trim(substr(replace(lower(local.app_name), "/[^a-z0-9-]+/", "-"), 0, 5), "-")}-${substr(md5(local.app_name), 0, 5)}-redis"
44
+ description = "Valkey cache for ${local.app_name}"
45
+ engine = "valkey"
46
+ engine_version = "8.0"
47
+ node_type = "cache.t4g.micro"
48
+ num_cache_clusters = 1
49
+ parameter_group_name = "default.valkey8"
50
+ port = 6379
51
+ subnet_group_name = aws_elasticache_subnet_group.redis.name
52
+ security_group_ids = [aws_security_group.redis.id]
53
+ automatic_failover_enabled = false
54
+ tags = { Name = "${local.app_name}-redis" }
55
+ }
56
+
57
+ # --- Outputs ---
58
+ output "redis_endpoint" {
59
+ value = "redis://${aws_elasticache_replication_group.redis.primary_endpoint_address}:${aws_elasticache_replication_group.redis.port}"
60
+ }
61
+
62
+ output "redis_port" {
63
+ value = aws_elasticache_replication_group.redis.port
64
+ }