@biffo/cli 0.38.0 → 0.41.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.
- package/_skeletons/plugin-template/.github/workflows/ci.yml +124 -0
- package/_skeletons/plugin-template/.github/workflows/release.yml +114 -0
- package/_skeletons/plugin-template/README.md +232 -0
- package/_skeletons/plugin-template/biffo.plugin.json +89 -0
- package/_skeletons/plugin-template/pyproject.toml +100 -0
- package/_skeletons/plugin-template/registry-schema.json +294 -0
- package/_skeletons/plugin-template/src/__init__.py +0 -0
- package/_skeletons/plugin-template/src/example_plugin/__init__.py +10 -0
- package/_skeletons/plugin-template/src/example_plugin/main.py +63 -0
- package/_skeletons/plugin-template/src/example_plugin/manifest.py +30 -0
- package/_skeletons/plugin-template/src/example_plugin/plugin.py +92 -0
- package/_skeletons/plugin-template/terraform/README.md +146 -0
- package/_skeletons/plugin-template/terraform/main.tf +167 -0
- package/_skeletons/plugin-template/terraform/outputs.tf +33 -0
- package/_skeletons/plugin-template/terraform/variables.tf +133 -0
- package/_skeletons/plugin-template/tests/conftest.py +17 -0
- package/_skeletons/plugin-template/tests/fakes.py +74 -0
- package/_skeletons/plugin-template/tests/test_example_plugin.py +108 -0
- package/_skeletons/registry/README.md +27 -0
- package/_skeletons/registry/plugins.json +5 -0
- package/_skeletons/registry/registry-schema.json +294 -0
- package/_skeletons/sibling-template/.github/renovate.json +38 -0
- package/_skeletons/sibling-template/.github/workflows/ci.yml +185 -0
- package/_skeletons/sibling-template/.github/workflows/codeql.yml +56 -0
- package/_skeletons/sibling-template/.github/workflows/deploy.yml +219 -0
- package/_skeletons/sibling-template/.github/workflows/destroy-infra.yml +73 -0
- package/_skeletons/sibling-template/README.md +164 -0
- package/_skeletons/sibling-template/_gitignore +71 -0
- package/_skeletons/sibling-template/apps/frontend/.env.example +14 -0
- package/_skeletons/sibling-template/apps/frontend/eslint.config.mjs +10 -0
- package/_skeletons/sibling-template/apps/frontend/next.config.ts +20 -0
- package/_skeletons/sibling-template/apps/frontend/package.json +42 -0
- package/_skeletons/sibling-template/apps/frontend/pnpm-lock.yaml +5268 -0
- package/_skeletons/sibling-template/apps/frontend/pnpm-workspace.yaml +19 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/globals.css +34 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/layout.tsx +15 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/page.test.tsx +75 -0
- package/_skeletons/sibling-template/apps/frontend/src/app/page.tsx +85 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/api-client.ts +58 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/auth.test.ts +135 -0
- package/_skeletons/sibling-template/apps/frontend/src/lib/auth.ts +87 -0
- package/_skeletons/sibling-template/apps/frontend/src/test-setup.ts +1 -0
- package/_skeletons/sibling-template/apps/frontend/tsconfig.json +29 -0
- package/_skeletons/sibling-template/apps/frontend/vitest.config.ts +18 -0
- package/_skeletons/sibling-template/biffo.sibling.json +7 -0
- package/_skeletons/sibling-template/infra/backend.tf +23 -0
- package/_skeletons/sibling-template/infra/main.tf +79 -0
- package/_skeletons/sibling-template/infra/outputs.tf +17 -0
- package/_skeletons/sibling-template/infra/variables.tf +63 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/main.tf +114 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/outputs.tf +12 -0
- package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/variables.tf +41 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/main.tf +157 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/outputs.tf +4 -0
- package/_skeletons/sibling-template/modules/cloud/aws/compute/variables.tf +70 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/main.tf +78 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/outputs.tf +4 -0
- package/_skeletons/sibling-template/modules/cloud/aws/storage/variables.tf +12 -0
- package/_skeletons/sibling-template/services/api/pyproject.toml +69 -0
- package/_skeletons/sibling-template/services/api/src/api/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/config.py +31 -0
- package/_skeletons/sibling-template/services/api/src/api/core_client.py +64 -0
- package/_skeletons/sibling-template/services/api/src/api/main.py +43 -0
- package/_skeletons/sibling-template/services/api/src/api/middleware/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/middleware/auth.py +111 -0
- package/_skeletons/sibling-template/services/api/src/api/routers/__init__.py +0 -0
- package/_skeletons/sibling-template/services/api/src/api/routers/whoami.py +21 -0
- package/_skeletons/sibling-template/services/api/tests/conftest.py +24 -0
- package/_skeletons/sibling-template/services/api/tests/test_whoami.py +21 -0
- package/_skeletons/sibling-template/services/api/uv.lock +1160 -0
- package/core.version +1 -1
- package/dist/index.js +113 -87
- package/package.json +5 -4
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Mirrors the core project's deploy-infra.yml + deploy-app.yml shape, but
|
|
2
|
+
# combined into one workflow with one job per phase (not one job pair PER
|
|
3
|
+
# environment, unlike the core project) — this repo is small enough that a
|
|
4
|
+
# single dynamic environment mapping (branch -> GitHub Environment) covers
|
|
5
|
+
# dev/staging/prod without tripling the job definitions. Each GitHub
|
|
6
|
+
# Environment (dev/staging/prod) still gets its own vars/secrets/required
|
|
7
|
+
# reviewers exactly as `biffo sibling create` configures via
|
|
8
|
+
# GitHubAdapter.createEnvironments — same governance, less duplicated YAML.
|
|
9
|
+
name: Deploy
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
branches: [dev, staging, main]
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
inputs:
|
|
16
|
+
environment:
|
|
17
|
+
description: Target environment
|
|
18
|
+
required: true
|
|
19
|
+
type: choice
|
|
20
|
+
options: [dev, staging, prod]
|
|
21
|
+
|
|
22
|
+
# Never cancel-in-progress here (unlike ci.yml) — this workflow runs
|
|
23
|
+
# `terraform apply` and ships application code. A second trigger for the
|
|
24
|
+
# same branch (e.g. a push landing right after a manual re-run) queues
|
|
25
|
+
# behind the first instead of running alongside it or cancelling a live
|
|
26
|
+
# apply mid-flight.
|
|
27
|
+
concurrency:
|
|
28
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
29
|
+
cancel-in-progress: false
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write
|
|
33
|
+
contents: read
|
|
34
|
+
|
|
35
|
+
env:
|
|
36
|
+
NODE_VERSION: '22'
|
|
37
|
+
TERRAFORM_VERSION: '~> 1.9'
|
|
38
|
+
|
|
39
|
+
jobs:
|
|
40
|
+
resolve-environment:
|
|
41
|
+
name: Resolve target environment
|
|
42
|
+
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
|
|
43
|
+
outputs:
|
|
44
|
+
environment: ${{ steps.resolve.outputs.environment }}
|
|
45
|
+
steps:
|
|
46
|
+
- id: resolve
|
|
47
|
+
run: |
|
|
48
|
+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
49
|
+
echo "environment=${{ inputs.environment }}" >> "$GITHUB_OUTPUT"
|
|
50
|
+
elif [ "${{ github.ref_name }}" = "main" ]; then
|
|
51
|
+
echo "environment=prod" >> "$GITHUB_OUTPUT"
|
|
52
|
+
else
|
|
53
|
+
echo "environment=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
deploy-infra:
|
|
57
|
+
name: Deploy infra
|
|
58
|
+
needs: resolve-environment
|
|
59
|
+
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
|
|
60
|
+
environment: ${{ needs.resolve-environment.outputs.environment }}
|
|
61
|
+
if: vars.SIBLING_DEPLOY_ENABLED == 'true'
|
|
62
|
+
defaults:
|
|
63
|
+
run:
|
|
64
|
+
working-directory: infra
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
67
|
+
- uses: hashicorp/setup-terraform@v4
|
|
68
|
+
with:
|
|
69
|
+
terraform_version: ${{ env.TERRAFORM_VERSION }}
|
|
70
|
+
- uses: aws-actions/configure-aws-credentials@v6
|
|
71
|
+
with:
|
|
72
|
+
role-to-assume: ${{ secrets.SIBLING_OIDC_ROLE_ARN }}
|
|
73
|
+
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
|
|
74
|
+
- name: Generate backend config
|
|
75
|
+
run: |
|
|
76
|
+
cat > backend.hcl << 'EOF'
|
|
77
|
+
bucket = "${{ vars.TF_STATE_BUCKET }}"
|
|
78
|
+
key = "${{ needs.resolve-environment.outputs.environment }}/terraform.tfstate"
|
|
79
|
+
region = "${{ vars.AWS_REGION || 'us-east-1' }}"
|
|
80
|
+
EOF
|
|
81
|
+
- run: terraform init -backend-config=backend.hcl
|
|
82
|
+
- run: terraform apply -auto-approve
|
|
83
|
+
env:
|
|
84
|
+
TF_VAR_project_name: ${{ vars.PROJECT_NAME }}
|
|
85
|
+
TF_VAR_environment: ${{ needs.resolve-environment.outputs.environment }}
|
|
86
|
+
TF_VAR_aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
|
|
87
|
+
TF_VAR_core_cognito_user_pool_id: ${{ vars.CORE_COGNITO_USER_POOL_ID }}
|
|
88
|
+
TF_VAR_core_cognito_client_id: ${{ vars.CORE_COGNITO_CLIENT_ID }}
|
|
89
|
+
TF_VAR_core_api_url: ${{ vars.CORE_API_URL }}
|
|
90
|
+
TF_VAR_core_portal_url: ${{ vars.CORE_PORTAL_URL }}
|
|
91
|
+
TF_VAR_cors_origins: ${{ vars.CORS_ORIGINS_JSON || '["http://localhost:3000"]' }}
|
|
92
|
+
# Empty until the core project's registration PR has merged — see
|
|
93
|
+
# variables.tf. Re-run this workflow (workflow_dispatch) once
|
|
94
|
+
# that PR merges and this var is set, to add the bucket policy.
|
|
95
|
+
TF_VAR_parent_cloudfront_distribution_arn: ${{ vars.PARENT_CLOUDFRONT_DISTRIBUTION_ARN || '' }}
|
|
96
|
+
- name: Export outputs to GitHub Actions variables
|
|
97
|
+
env:
|
|
98
|
+
GH_TOKEN: ${{ secrets.SIBLING_GITHUB_TOKEN }}
|
|
99
|
+
GH_REPO: ${{ github.repository }}
|
|
100
|
+
run: |
|
|
101
|
+
gh variable set API_URL \
|
|
102
|
+
--body "$(terraform output -raw api_url)" \
|
|
103
|
+
--env "${{ needs.resolve-environment.outputs.environment }}"
|
|
104
|
+
gh variable set SITE_BUCKET_NAME \
|
|
105
|
+
--body "$(terraform output -raw site_bucket_name)" \
|
|
106
|
+
--env "${{ needs.resolve-environment.outputs.environment }}"
|
|
107
|
+
gh variable set SITE_BUCKET_REGIONAL_DOMAIN \
|
|
108
|
+
--body "$(terraform output -raw site_bucket_regional_domain)" \
|
|
109
|
+
--env "${{ needs.resolve-environment.outputs.environment }}"
|
|
110
|
+
gh variable set LAMBDA_FUNCTION_NAME \
|
|
111
|
+
--body "$(terraform output -raw lambda_function_name)" \
|
|
112
|
+
--env "${{ needs.resolve-environment.outputs.environment }}"
|
|
113
|
+
|
|
114
|
+
deploy-app:
|
|
115
|
+
name: Deploy app
|
|
116
|
+
needs: [resolve-environment, deploy-infra]
|
|
117
|
+
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
|
|
118
|
+
environment: ${{ needs.resolve-environment.outputs.environment }}
|
|
119
|
+
steps:
|
|
120
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
121
|
+
- uses: pnpm/action-setup@v4
|
|
122
|
+
with:
|
|
123
|
+
version: 9.15.9
|
|
124
|
+
- uses: actions/setup-node@v4
|
|
125
|
+
with:
|
|
126
|
+
node-version: ${{ env.NODE_VERSION }}
|
|
127
|
+
cache: pnpm
|
|
128
|
+
cache-dependency-path: apps/frontend/pnpm-lock.yaml
|
|
129
|
+
- uses: astral-sh/setup-uv@v5
|
|
130
|
+
with:
|
|
131
|
+
enable-cache: true
|
|
132
|
+
- name: Install frontend deps
|
|
133
|
+
working-directory: apps/frontend
|
|
134
|
+
run: pnpm install --frozen-lockfile
|
|
135
|
+
- name: Build frontend
|
|
136
|
+
working-directory: apps/frontend
|
|
137
|
+
env:
|
|
138
|
+
NEXT_PUBLIC_SIBLING_NAME: ${{ vars.PROJECT_NAME }}
|
|
139
|
+
# The ROOT application sibling has an EMPTY PATH_PREFIX (issue #306):
|
|
140
|
+
# it serves "/" and takes the core distribution's default cache
|
|
141
|
+
# behaviour. `/${{ vars.PATH_PREFIX }}` would yield "/" for it, and
|
|
142
|
+
# Next.js refuses to build with a basePath of "/" ("has to be a path
|
|
143
|
+
# starting with /, but not ending with one"). Empty string is the
|
|
144
|
+
# correct value there, so the prefix is only prepended when there is
|
|
145
|
+
# one.
|
|
146
|
+
NEXT_PUBLIC_SIBLING_PATH_PREFIX: ${{ vars.PATH_PREFIX && format('/{0}', vars.PATH_PREFIX) || '' }}
|
|
147
|
+
NEXT_PUBLIC_BASE_PATH: ${{ vars.PATH_PREFIX && format('/{0}', vars.PATH_PREFIX) || '' }}
|
|
148
|
+
NEXT_PUBLIC_CORE_PORTAL_URL: ${{ vars.CORE_PORTAL_URL }}
|
|
149
|
+
NEXT_PUBLIC_CORE_COGNITO_USER_POOL_ID: ${{ vars.CORE_COGNITO_USER_POOL_ID }}
|
|
150
|
+
NEXT_PUBLIC_CORE_COGNITO_CLIENT_ID: ${{ vars.CORE_COGNITO_CLIENT_ID }}
|
|
151
|
+
NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
|
|
152
|
+
run: pnpm run build
|
|
153
|
+
- uses: aws-actions/configure-aws-credentials@v6
|
|
154
|
+
with:
|
|
155
|
+
role-to-assume: ${{ secrets.SIBLING_OIDC_ROLE_ARN }}
|
|
156
|
+
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
|
|
157
|
+
- name: Sync frontend to S3
|
|
158
|
+
env:
|
|
159
|
+
PATH_PREFIX: ${{ vars.PATH_PREFIX }}
|
|
160
|
+
SITE_BUCKET_NAME: ${{ vars.SITE_BUCKET_NAME }}
|
|
161
|
+
run: |
|
|
162
|
+
# Uploaded under a key prefix matching this sibling's own path
|
|
163
|
+
# segment (basePath), because the core project's CloudFront
|
|
164
|
+
# forwards the full request URI (e.g. /<name>/foo.html) straight
|
|
165
|
+
# through to this bucket with no prefix stripping.
|
|
166
|
+
#
|
|
167
|
+
# ${VAR:+...} — expand to "<prefix>/" only when the prefix is
|
|
168
|
+
# non-empty. The ROOT application sibling (issue #306) has an empty
|
|
169
|
+
# prefix and syncs to the bucket root; the naive "$BUCKET/$PREFIX/"
|
|
170
|
+
# would give "s3://bucket//", whose keys all begin with a literal
|
|
171
|
+
# slash and match no request CloudFront ever forwards.
|
|
172
|
+
DEST="s3://${SITE_BUCKET_NAME}/${PATH_PREFIX:+${PATH_PREFIX}/}"
|
|
173
|
+
aws s3 sync apps/frontend/out/ "$DEST" \
|
|
174
|
+
--delete \
|
|
175
|
+
--cache-control "public, max-age=31536000, immutable" \
|
|
176
|
+
--exclude "*.html"
|
|
177
|
+
aws s3 sync apps/frontend/out/ "$DEST" \
|
|
178
|
+
--delete \
|
|
179
|
+
--cache-control "no-cache" \
|
|
180
|
+
--include "*.html"
|
|
181
|
+
- name: Invalidate the core project's CloudFront path
|
|
182
|
+
if: vars.PARENT_CLOUDFRONT_DISTRIBUTION_ID != ''
|
|
183
|
+
env:
|
|
184
|
+
PATH_PREFIX: ${{ vars.PATH_PREFIX }}
|
|
185
|
+
DISTRIBUTION_ID: ${{ vars.PARENT_CLOUDFRONT_DISTRIBUTION_ID }}
|
|
186
|
+
run: |
|
|
187
|
+
# "/*" for the root sibling, "/<prefix>/*" otherwise.
|
|
188
|
+
aws cloudfront create-invalidation \
|
|
189
|
+
--distribution-id "$DISTRIBUTION_ID" \
|
|
190
|
+
--paths "/${PATH_PREFIX:+${PATH_PREFIX}/}*"
|
|
191
|
+
- uses: astral-sh/setup-uv@v5
|
|
192
|
+
with:
|
|
193
|
+
python-version: '3.13'
|
|
194
|
+
- name: Package and deploy Lambda
|
|
195
|
+
working-directory: services/api
|
|
196
|
+
run: |
|
|
197
|
+
uv export --no-dev --output-file requirements.txt
|
|
198
|
+
uv pip install -r requirements.txt --target package/
|
|
199
|
+
cp -r src/ package/
|
|
200
|
+
cd package && zip -r ../lambda.zip .
|
|
201
|
+
aws lambda update-function-code \
|
|
202
|
+
--function-name "${{ vars.LAMBDA_FUNCTION_NAME }}" \
|
|
203
|
+
--zip-file fileb://../lambda.zip
|
|
204
|
+
- name: Smoke test the deployed Lambda
|
|
205
|
+
run: |
|
|
206
|
+
# Deploy succeeding just means the zip uploaded — it says nothing
|
|
207
|
+
# about whether the Lambda can actually boot (e.g. a native
|
|
208
|
+
# dependency built for the wrong Python ABI fails at import time,
|
|
209
|
+
# not at deploy time). GET /api/v1/health is unauthenticated by
|
|
210
|
+
# design specifically so this check can hit the real API Gateway
|
|
211
|
+
# route with no token, proving the Lambda imports and serves a
|
|
212
|
+
# request end to end, not just that code was uploaded.
|
|
213
|
+
aws lambda wait function-updated \
|
|
214
|
+
--function-name "${{ vars.LAMBDA_FUNCTION_NAME }}"
|
|
215
|
+
if ! curl -sf --retry 5 --retry-delay 3 --retry-all-errors \
|
|
216
|
+
"${{ vars.API_URL }}/api/v1/health"; then
|
|
217
|
+
echo "::error::Health check failed after deploy — the Lambda uploaded but isn't serving requests. Check CloudWatch logs for ${{ vars.LAMBDA_FUNCTION_NAME }}."
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Counterpart to deploy.yml, and the mechanism `biffo teardown` drives when the
|
|
2
|
+
# core project it belongs to is torn down (issue #306).
|
|
3
|
+
#
|
|
4
|
+
# Without this workflow a sibling's own AWS resources — its S3 site bucket, its
|
|
5
|
+
# Lambda, its API Gateway, its log groups — survive the core project's teardown
|
|
6
|
+
# and keep billing, silently. `biffo teardown` pre-flights every registered
|
|
7
|
+
# sibling for this file and refuses to start if one is missing, rather than
|
|
8
|
+
# discovering it half way through and leaving a partially destroyed instance.
|
|
9
|
+
#
|
|
10
|
+
# Same single-dynamic-environment shape as deploy.yml (one job per phase, not
|
|
11
|
+
# one job per environment), and the TF_VARs below must stay in step with
|
|
12
|
+
# deploy.yml's `terraform apply` — a destroy run with different variables can
|
|
13
|
+
# fail to resolve the very resources it is meant to remove.
|
|
14
|
+
name: Destroy Infrastructure
|
|
15
|
+
|
|
16
|
+
on:
|
|
17
|
+
workflow_dispatch:
|
|
18
|
+
inputs:
|
|
19
|
+
environment:
|
|
20
|
+
description: Environment to destroy
|
|
21
|
+
required: true
|
|
22
|
+
type: choice
|
|
23
|
+
options: [dev, staging, prod]
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
id-token: write
|
|
27
|
+
contents: read
|
|
28
|
+
|
|
29
|
+
# Never cancel in flight: a half-run `terraform destroy` is worse than a slow one.
|
|
30
|
+
concurrency:
|
|
31
|
+
group: ${{ github.workflow }}-${{ inputs.environment }}
|
|
32
|
+
cancel-in-progress: false
|
|
33
|
+
|
|
34
|
+
env:
|
|
35
|
+
TERRAFORM_VERSION: '~> 1.9'
|
|
36
|
+
|
|
37
|
+
jobs:
|
|
38
|
+
destroy:
|
|
39
|
+
name: Destroy (${{ inputs.environment }})
|
|
40
|
+
runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
|
|
41
|
+
environment: ${{ inputs.environment }}
|
|
42
|
+
if: vars.SIBLING_DEPLOY_ENABLED == 'true'
|
|
43
|
+
defaults:
|
|
44
|
+
run:
|
|
45
|
+
working-directory: infra
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
|
48
|
+
- uses: hashicorp/setup-terraform@v4
|
|
49
|
+
with:
|
|
50
|
+
terraform_version: ${{ env.TERRAFORM_VERSION }}
|
|
51
|
+
- uses: aws-actions/configure-aws-credentials@v6
|
|
52
|
+
with:
|
|
53
|
+
role-to-assume: ${{ secrets.SIBLING_OIDC_ROLE_ARN }}
|
|
54
|
+
aws-region: ${{ vars.AWS_REGION || 'us-east-1' }}
|
|
55
|
+
- name: Generate backend config
|
|
56
|
+
run: |
|
|
57
|
+
cat > backend.hcl << 'EOF'
|
|
58
|
+
bucket = "${{ vars.TF_STATE_BUCKET }}"
|
|
59
|
+
key = "${{ inputs.environment }}/terraform.tfstate"
|
|
60
|
+
region = "${{ vars.AWS_REGION || 'us-east-1' }}"
|
|
61
|
+
EOF
|
|
62
|
+
- run: terraform init -backend-config=backend.hcl
|
|
63
|
+
- run: terraform destroy -auto-approve
|
|
64
|
+
env:
|
|
65
|
+
TF_VAR_project_name: ${{ vars.PROJECT_NAME }}
|
|
66
|
+
TF_VAR_environment: ${{ inputs.environment }}
|
|
67
|
+
TF_VAR_aws_region: ${{ vars.AWS_REGION || 'us-east-1' }}
|
|
68
|
+
TF_VAR_core_cognito_user_pool_id: ${{ vars.CORE_COGNITO_USER_POOL_ID }}
|
|
69
|
+
TF_VAR_core_cognito_client_id: ${{ vars.CORE_COGNITO_CLIENT_ID }}
|
|
70
|
+
TF_VAR_core_api_url: ${{ vars.CORE_API_URL }}
|
|
71
|
+
TF_VAR_core_portal_url: ${{ vars.CORE_PORTAL_URL }}
|
|
72
|
+
TF_VAR_cors_origins: ${{ vars.CORS_ORIGINS_JSON || '["http://localhost:3000"]' }}
|
|
73
|
+
TF_VAR_parent_cloudfront_distribution_arn: ${{ vars.PARENT_CLOUDFRONT_DISTRIBUTION_ARN || '' }}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Sibling App Template
|
|
2
|
+
|
|
3
|
+
> **This is a skeleton, not a live repository.** It lives at
|
|
4
|
+
> `_skeletons/sibling-template/` inside `biffo-template` and is not itself
|
|
5
|
+
> deployed or a member of biffo-template's pnpm/uv workspace — check
|
|
6
|
+
> `pnpm-workspace.yaml` (only lists `apps/*`, `packages/*`, `cli`) and the
|
|
7
|
+
> root `pyproject.toml`'s `[tool.uv.workspace]` (only lists `services/*`,
|
|
8
|
+
> `services/_plugins/*`,
|
|
9
|
+
> `packages/python-sdk`); neither lists `_skeletons/`. It exists to be
|
|
10
|
+
> copied into a **brand-new, independent GitHub repository** by
|
|
11
|
+
> `biffo sibling create <name>` (ADR-0007), which pushes this content in as
|
|
12
|
+
> that new repo's first commit and rewrites `biffo.sibling.json` with real
|
|
13
|
+
> values. No such external repo exists yet from this skeleton alone — that's
|
|
14
|
+
> the point of it being a skeleton, not an oversight.
|
|
15
|
+
>
|
|
16
|
+
> To use it manually (outside `biffo sibling create`): copy this directory
|
|
17
|
+
> out, rename the placeholder values in `biffo.sibling.json` and the two
|
|
18
|
+
> `.env.example` files, then follow "Getting started" below.
|
|
19
|
+
|
|
20
|
+
## What is a "sibling app"?
|
|
21
|
+
|
|
22
|
+
A sibling is an independently-deployed microservice that still feels like
|
|
23
|
+
part of one product: same login (it shares the **core project's** Cognito
|
|
24
|
+
User Pool and App Client — not its own), same domain
|
|
25
|
+
(`baseurl.com/<sibling-name>/*`, routed via the core project's own
|
|
26
|
+
CloudFront distribution), but its own repo, its own CI/CD, its own AWS
|
|
27
|
+
resources. Per ADR-0002 and ADR-0007, a sibling **never accesses a database
|
|
28
|
+
directly** — the only way it reads or writes core-owned data is by calling
|
|
29
|
+
the core project's own API (`services/api/core_client.py`). This template
|
|
30
|
+
enforces that from the start: there is no `asyncpg`/`sqlalchemy`/`alembic`
|
|
31
|
+
anywhere in `services/api/`, on purpose.
|
|
32
|
+
|
|
33
|
+
## What's here
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
apps/frontend/ # Next.js 15 static export — the ONLY page is "<name> - Hello <username>",
|
|
37
|
+
# proving the shared-session SSO works. Copy the portal's own
|
|
38
|
+
# src/lib/auth.ts pattern here almost verbatim (see that file's comments).
|
|
39
|
+
services/api/ # FastAPI + Mangum backend. Verifies the core project's Cognito JWT itself
|
|
40
|
+
# (defense in depth — API Gateway's own JWT authorizer is the first layer).
|
|
41
|
+
# core_client.py is the ONLY sanctioned way to reach core-owned data.
|
|
42
|
+
infra/ # Single Terraform root (no per-environment subfolders, unlike the core
|
|
43
|
+
# project — this repo is simple enough that one root + a Terraform
|
|
44
|
+
# workspace/backend key per environment is enough). No Cognito pool,
|
|
45
|
+
# no CloudFront distribution: those are the core project's, passed in
|
|
46
|
+
# as plain input variables (core_cognito_user_pool_id, etc.)
|
|
47
|
+
modules/cloud/aws/ # Vendored (copied, not remote-sourced) compute/storage/api-gateway
|
|
48
|
+
# modules — this repo must stay self-contained; a git-sourced shared
|
|
49
|
+
# module would reintroduce the cross-repo-lifecycle coupling ADR-0003
|
|
50
|
+
# already rejected for plugins. Same call, made consistently here.
|
|
51
|
+
.github/workflows/ci.yml # Same 11 status checks as the core project (see DEFAULT_STATUS_CHECKS
|
|
52
|
+
# in the core project's cli/src/adapters/source-control/github/index.ts)
|
|
53
|
+
.github/workflows/deploy.yml # infra apply -> frontend build+sync -> Lambda package+deploy, combined
|
|
54
|
+
# into one workflow with a dynamic branch->environment mapping.
|
|
55
|
+
biffo.sibling.json # This sibling's name, its paired core project's name, its path prefix.
|
|
56
|
+
# Written by `biffo sibling create`; do not hand-edit path_prefix
|
|
57
|
+
# without also updating the core project's siblings.auto.tfvars.json.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Standalone repo, not a monorepo package
|
|
61
|
+
|
|
62
|
+
Both `apps/frontend/package.json` and `services/api/pyproject.toml` are
|
|
63
|
+
fully standalone — no `workspace:*` dependencies, no Turborepo, no shared
|
|
64
|
+
`@biffo/eslint-config`/`@biffo/typescript-config` packages (those only exist
|
|
65
|
+
inside the `biffo-template` monorepo). `services/api/pyproject.toml` has its
|
|
66
|
+
own `[build-system]` (hatchling) so `uv sync` works from a bare clone. This
|
|
67
|
+
means `apps/frontend/eslint.config.mjs` and `tsconfig.json` duplicate rules
|
|
68
|
+
the core project's shared packages would otherwise centralise — an
|
|
69
|
+
intentional, small amount of duplication in exchange for this repo never
|
|
70
|
+
needing anything from `biffo-template` at runtime or build time.
|
|
71
|
+
|
|
72
|
+
`apps/frontend/pnpm-workspace.yaml` exists for the same reason, and is worth
|
|
73
|
+
understanding before you delete it as redundant. `pnpm install` walks _up_ the
|
|
74
|
+
directory tree looking for a workspace root. In a scaffolded sibling repo there
|
|
75
|
+
is nothing above `apps/frontend` to find, so the file is inert. But while this
|
|
76
|
+
skeleton still lives inside `biffo-template` — under `_skeletons/`, excluded
|
|
77
|
+
from that repo's own workspace globs — `pnpm install` run from here would
|
|
78
|
+
otherwise walk all the way up and install **biffo-template's root workspace**
|
|
79
|
+
instead: it prints success against `../../../..`, leaves no `node_modules` here
|
|
80
|
+
at all, and the next command fails for a reason that looks entirely unrelated.
|
|
81
|
+
Declaring this directory a workspace root of its own terminates that walk.
|
|
82
|
+
(A `preinstall` guard cannot help: pnpm never treats an excluded package as
|
|
83
|
+
part of the install, so its lifecycle scripts never run.)
|
|
84
|
+
|
|
85
|
+
## The build must not need Cognito credentials
|
|
86
|
+
|
|
87
|
+
`pnpm run build` has to succeed with **no** `NEXT_PUBLIC_CORE_COGNITO_*` set,
|
|
88
|
+
and `.github/workflows/ci.yml` runs it that way on every PR to keep it that
|
|
89
|
+
way. `next build` prerenders `/` in Node, which imports `src/lib/auth.ts`; that
|
|
90
|
+
module therefore constructs its Cognito user pool lazily, on first session
|
|
91
|
+
read, never at module scope (the `CognitoUserPool` constructor throws outright
|
|
92
|
+
when either id is missing). If you add module-scope code that requires real
|
|
93
|
+
core config, the build breaks for everyone who has not exported the pool ids —
|
|
94
|
+
starting with CI. Read config inside the function that needs it.
|
|
95
|
+
|
|
96
|
+
## The two-phase CDN registration
|
|
97
|
+
|
|
98
|
+
Registering this sibling on the core project's CloudFront distribution is
|
|
99
|
+
necessarily a two-step handshake, not something this repo's own
|
|
100
|
+
`terraform apply` can complete alone:
|
|
101
|
+
|
|
102
|
+
1. `biffo sibling create` provisions this sibling's own S3 bucket/Lambda/API
|
|
103
|
+
Gateway first (`var.parent_cloudfront_distribution_arn` left empty — see
|
|
104
|
+
`infra/variables.tf` — so the bucket policy step is skipped; there's no
|
|
105
|
+
distribution ARN to trust yet).
|
|
106
|
+
2. It then opens a PR against the **core project's** repo, appending this
|
|
107
|
+
sibling's `{ name, bucket_regional_domain }` to
|
|
108
|
+
`infra/siblings.auto.tfvars.json`. Once a human merges that PR and the
|
|
109
|
+
core project redeploys, `baseurl.com/<name>/*` starts routing to this
|
|
110
|
+
sibling's bucket, and the real distribution ARN exists.
|
|
111
|
+
3. Set `parent_cloudfront_distribution_arn` (as `PARENT_CLOUDFRONT_DISTRIBUTION_ARN`,
|
|
112
|
+
a GitHub Environment variable) and re-run `.github/workflows/deploy.yml`
|
|
113
|
+
(`workflow_dispatch`) to add the bucket policy in a second apply.
|
|
114
|
+
|
|
115
|
+
This ordering is a real dependency, not a bug — don't skip step 3 or the
|
|
116
|
+
core project's CloudFront will get an origin it isn't allowed to read from.
|
|
117
|
+
|
|
118
|
+
## Contribution guidelines
|
|
119
|
+
|
|
120
|
+
- Conventional Commits (`feat`, `fix`, `chore`, `docs`, `test`, `infra`,
|
|
121
|
+
`security`, `refactor`, `perf`, `ci`), same as the core project.
|
|
122
|
+
- Keep this repo doing one thing: this sibling's own feature. If you find
|
|
123
|
+
yourself wanting to read/write another sibling's data, go through the
|
|
124
|
+
core project's API and an EventBridge subscription, not a direct call.
|
|
125
|
+
- Invariants inherited from the core project (ADR-0001/ADR-0002): every
|
|
126
|
+
piece of core-owned data this sibling touches is still scoped to
|
|
127
|
+
`tenant_id` server-side (the core API enforces this, not this repo) — and
|
|
128
|
+
no database client of any kind belongs in `services/api/`.
|
|
129
|
+
- Before opening a PR:
|
|
130
|
+
```bash
|
|
131
|
+
cd apps/frontend && pnpm install && pnpm run lint && pnpm run typecheck && pnpm run test && pnpm run build
|
|
132
|
+
cd services/api && uv sync --all-groups && uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run pytest
|
|
133
|
+
terraform fmt -check -recursive infra/ modules/
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Branch protection setup
|
|
137
|
+
|
|
138
|
+
`biffo sibling create` configures this via the same
|
|
139
|
+
`GitHubAdapter.configureBranchProtection` the core project uses (passing
|
|
140
|
+
this repo's own `statusChecks` list — see that method's third parameter):
|
|
141
|
+
|
|
142
|
+
| Setting | Value |
|
|
143
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
144
|
+
| Required status checks | The 11 jobs in `.github/workflows/ci.yml`, `strict: true` |
|
|
145
|
+
| Required reviews | 0 (solo-founder default; raise this yourself) |
|
|
146
|
+
| Dismiss stale reviews | true |
|
|
147
|
+
| Enforce for admins | true |
|
|
148
|
+
| Linear history | true |
|
|
149
|
+
| Allow force pushes | false |
|
|
150
|
+
| Allow deletions | false |
|
|
151
|
+
|
|
152
|
+
## Getting started (manual, outside `biffo sibling create`)
|
|
153
|
+
|
|
154
|
+
1. Rename `biffo.sibling.json`'s `name`/`core_project`/`path_prefix`.
|
|
155
|
+
2. Copy `apps/frontend/.env.example` to `.env.local` and fill in your core
|
|
156
|
+
project's real Cognito/API values for local dev.
|
|
157
|
+
3. `cd infra && terraform init -backend-config=backend.hcl && terraform apply`
|
|
158
|
+
(generate `backend.hcl` yourself for local use — CI generates it inline,
|
|
159
|
+
see `deploy.yml`).
|
|
160
|
+
4. Wire the GitHub Environment variables/secrets `deploy.yml` reads
|
|
161
|
+
(`CORE_COGNITO_USER_POOL_ID`, `CORE_COGNITO_CLIENT_ID`, `CORE_API_URL`,
|
|
162
|
+
`CORE_PORTAL_URL`, `SIBLING_OIDC_ROLE_ARN`, `TF_STATE_BUCKET`, etc.).
|
|
163
|
+
5. Open the registration PR against your core project (`infra/siblings.auto.tfvars.json`)
|
|
164
|
+
yourself, or let `biffo sibling create` do it for you.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
.pnpm-store
|
|
4
|
+
|
|
5
|
+
# Build outputs
|
|
6
|
+
.next
|
|
7
|
+
dist
|
|
8
|
+
out
|
|
9
|
+
next-env.d.ts
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
__pycache__
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*.pyo
|
|
16
|
+
.venv
|
|
17
|
+
.uv
|
|
18
|
+
*.egg-info
|
|
19
|
+
.pytest_cache
|
|
20
|
+
.ruff_cache
|
|
21
|
+
.mypy_cache
|
|
22
|
+
.pyright
|
|
23
|
+
|
|
24
|
+
# Terraform
|
|
25
|
+
.terraform
|
|
26
|
+
.terraform.lock.hcl
|
|
27
|
+
*.tfstate
|
|
28
|
+
*.tfstate.*
|
|
29
|
+
*.tfstate.backup
|
|
30
|
+
crash.log
|
|
31
|
+
crash.*.log
|
|
32
|
+
override.tf
|
|
33
|
+
override.tf.json
|
|
34
|
+
*_override.tf
|
|
35
|
+
*_override.tf.json
|
|
36
|
+
.terraformrc
|
|
37
|
+
terraform.rc
|
|
38
|
+
tfplan
|
|
39
|
+
backend.hcl
|
|
40
|
+
|
|
41
|
+
# Biffo local project store — persisted by `biffo sibling create`, read by `biffo deploy`
|
|
42
|
+
.biffo/
|
|
43
|
+
|
|
44
|
+
# Environment & secrets — never commit these
|
|
45
|
+
.env
|
|
46
|
+
.env.*
|
|
47
|
+
!.env.example
|
|
48
|
+
!.env.test
|
|
49
|
+
*.pem
|
|
50
|
+
*.key
|
|
51
|
+
*.crt
|
|
52
|
+
*.p12
|
|
53
|
+
secrets/
|
|
54
|
+
|
|
55
|
+
# Editor
|
|
56
|
+
.vscode
|
|
57
|
+
.idea
|
|
58
|
+
*.swp
|
|
59
|
+
*.swo
|
|
60
|
+
.DS_Store
|
|
61
|
+
Thumbs.db
|
|
62
|
+
|
|
63
|
+
# Logs
|
|
64
|
+
*.log
|
|
65
|
+
npm-debug.log*
|
|
66
|
+
pnpm-debug.log*
|
|
67
|
+
|
|
68
|
+
# Test coverage
|
|
69
|
+
coverage
|
|
70
|
+
htmlcov
|
|
71
|
+
.coverage
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Set at Next.js build time (see .github/workflows/deploy.yml) — inlined
|
|
2
|
+
# into the static export, not read at runtime. All values below come from
|
|
3
|
+
# the CORE project, not this sibling's own Terraform (ADR-0007): a sibling
|
|
4
|
+
# has no Cognito pool or CloudFront distribution of its own.
|
|
5
|
+
NEXT_PUBLIC_SIBLING_NAME=example-sibling
|
|
6
|
+
NEXT_PUBLIC_SIBLING_PATH_PREFIX=/example-sibling
|
|
7
|
+
NEXT_PUBLIC_BASE_PATH=/example-sibling
|
|
8
|
+
NEXT_PUBLIC_CORE_PORTAL_URL=https://baseurl.com
|
|
9
|
+
NEXT_PUBLIC_CORE_COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX
|
|
10
|
+
NEXT_PUBLIC_CORE_COGNITO_CLIENT_ID=XXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
11
|
+
# This sibling's OWN API Gateway endpoint (from `terraform output api_url`),
|
|
12
|
+
# NOT the core project's API — the frontend never calls the core API
|
|
13
|
+
# directly (ADR-0002/ADR-0007).
|
|
14
|
+
NEXT_PUBLIC_API_URL=https://XXXXXXXXXX.execute-api.us-east-1.amazonaws.com
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FlatCompat } from '@eslint/eslintrc'
|
|
2
|
+
|
|
3
|
+
const compat = new FlatCompat({ baseDirectory: import.meta.dirname })
|
|
4
|
+
|
|
5
|
+
const config = [
|
|
6
|
+
{ ignores: ['.next/**', 'next-env.d.ts', 'out/**'] },
|
|
7
|
+
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export default config
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NextConfig } from 'next'
|
|
2
|
+
|
|
3
|
+
// basePath must match the "<name>" segment CloudFront routes on
|
|
4
|
+
// (baseurl.com/<name>/*, see infra/main.tf's sibling registration) — the
|
|
5
|
+
// parent's CDN forwards the full request URI (e.g. /<name>/foo.html)
|
|
6
|
+
// straight to this sibling's S3 origin with no prefix stripping, so the
|
|
7
|
+
// static export's own asset/link URLs must already include that prefix.
|
|
8
|
+
// Set at build time; see .github/workflows/ci.yml / deploy.yml.
|
|
9
|
+
const basePath = process.env['NEXT_PUBLIC_BASE_PATH'] ?? ''
|
|
10
|
+
|
|
11
|
+
const nextConfig: NextConfig = {
|
|
12
|
+
output: 'export',
|
|
13
|
+
trailingSlash: true,
|
|
14
|
+
basePath,
|
|
15
|
+
images: {
|
|
16
|
+
unoptimized: true,
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default nextConfig
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sibling-frontend",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev --turbopack",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"lint:fix": "eslint . --fix",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "vitest run"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"amazon-cognito-identity-js": "^6.3.12",
|
|
16
|
+
"next": "^15.1.3",
|
|
17
|
+
"react": "^19.0.0",
|
|
18
|
+
"react-dom": "^19.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
22
|
+
"@testing-library/dom": "^10.4.1",
|
|
23
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
24
|
+
"@testing-library/react": "^16.3.2",
|
|
25
|
+
"@types/node": "^22.10.5",
|
|
26
|
+
"@types/react": "^19.0.7",
|
|
27
|
+
"@types/react-dom": "^19.0.3",
|
|
28
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
29
|
+
"eslint": "^9.18.0",
|
|
30
|
+
"eslint-config-next": "^15.1.3",
|
|
31
|
+
"jsdom": "^29.1.1",
|
|
32
|
+
"prettier": "^3.4.2",
|
|
33
|
+
"typescript": "^5.7.3",
|
|
34
|
+
"vite": "^6.4.3",
|
|
35
|
+
"vitest": "^4.1.0"
|
|
36
|
+
},
|
|
37
|
+
"pnpm": {
|
|
38
|
+
"overrides": {
|
|
39
|
+
"postcss@<8.5.10": ">=8.5.10"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|