@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.
Files changed (73) hide show
  1. package/_skeletons/plugin-template/.github/workflows/ci.yml +124 -0
  2. package/_skeletons/plugin-template/.github/workflows/release.yml +114 -0
  3. package/_skeletons/plugin-template/README.md +232 -0
  4. package/_skeletons/plugin-template/biffo.plugin.json +89 -0
  5. package/_skeletons/plugin-template/pyproject.toml +100 -0
  6. package/_skeletons/plugin-template/registry-schema.json +294 -0
  7. package/_skeletons/plugin-template/src/__init__.py +0 -0
  8. package/_skeletons/plugin-template/src/example_plugin/__init__.py +10 -0
  9. package/_skeletons/plugin-template/src/example_plugin/main.py +63 -0
  10. package/_skeletons/plugin-template/src/example_plugin/manifest.py +30 -0
  11. package/_skeletons/plugin-template/src/example_plugin/plugin.py +92 -0
  12. package/_skeletons/plugin-template/terraform/README.md +146 -0
  13. package/_skeletons/plugin-template/terraform/main.tf +167 -0
  14. package/_skeletons/plugin-template/terraform/outputs.tf +33 -0
  15. package/_skeletons/plugin-template/terraform/variables.tf +133 -0
  16. package/_skeletons/plugin-template/tests/conftest.py +17 -0
  17. package/_skeletons/plugin-template/tests/fakes.py +74 -0
  18. package/_skeletons/plugin-template/tests/test_example_plugin.py +108 -0
  19. package/_skeletons/registry/README.md +27 -0
  20. package/_skeletons/registry/plugins.json +5 -0
  21. package/_skeletons/registry/registry-schema.json +294 -0
  22. package/_skeletons/sibling-template/.github/renovate.json +38 -0
  23. package/_skeletons/sibling-template/.github/workflows/ci.yml +185 -0
  24. package/_skeletons/sibling-template/.github/workflows/codeql.yml +56 -0
  25. package/_skeletons/sibling-template/.github/workflows/deploy.yml +219 -0
  26. package/_skeletons/sibling-template/.github/workflows/destroy-infra.yml +73 -0
  27. package/_skeletons/sibling-template/README.md +164 -0
  28. package/_skeletons/sibling-template/_gitignore +71 -0
  29. package/_skeletons/sibling-template/apps/frontend/.env.example +14 -0
  30. package/_skeletons/sibling-template/apps/frontend/eslint.config.mjs +10 -0
  31. package/_skeletons/sibling-template/apps/frontend/next.config.ts +20 -0
  32. package/_skeletons/sibling-template/apps/frontend/package.json +42 -0
  33. package/_skeletons/sibling-template/apps/frontend/pnpm-lock.yaml +5268 -0
  34. package/_skeletons/sibling-template/apps/frontend/pnpm-workspace.yaml +19 -0
  35. package/_skeletons/sibling-template/apps/frontend/src/app/globals.css +34 -0
  36. package/_skeletons/sibling-template/apps/frontend/src/app/layout.tsx +15 -0
  37. package/_skeletons/sibling-template/apps/frontend/src/app/page.test.tsx +75 -0
  38. package/_skeletons/sibling-template/apps/frontend/src/app/page.tsx +85 -0
  39. package/_skeletons/sibling-template/apps/frontend/src/lib/api-client.ts +58 -0
  40. package/_skeletons/sibling-template/apps/frontend/src/lib/auth.test.ts +135 -0
  41. package/_skeletons/sibling-template/apps/frontend/src/lib/auth.ts +87 -0
  42. package/_skeletons/sibling-template/apps/frontend/src/test-setup.ts +1 -0
  43. package/_skeletons/sibling-template/apps/frontend/tsconfig.json +29 -0
  44. package/_skeletons/sibling-template/apps/frontend/vitest.config.ts +18 -0
  45. package/_skeletons/sibling-template/biffo.sibling.json +7 -0
  46. package/_skeletons/sibling-template/infra/backend.tf +23 -0
  47. package/_skeletons/sibling-template/infra/main.tf +79 -0
  48. package/_skeletons/sibling-template/infra/outputs.tf +17 -0
  49. package/_skeletons/sibling-template/infra/variables.tf +63 -0
  50. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/main.tf +114 -0
  51. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/outputs.tf +12 -0
  52. package/_skeletons/sibling-template/modules/cloud/aws/api-gateway/variables.tf +41 -0
  53. package/_skeletons/sibling-template/modules/cloud/aws/compute/main.tf +157 -0
  54. package/_skeletons/sibling-template/modules/cloud/aws/compute/outputs.tf +4 -0
  55. package/_skeletons/sibling-template/modules/cloud/aws/compute/variables.tf +70 -0
  56. package/_skeletons/sibling-template/modules/cloud/aws/storage/main.tf +78 -0
  57. package/_skeletons/sibling-template/modules/cloud/aws/storage/outputs.tf +4 -0
  58. package/_skeletons/sibling-template/modules/cloud/aws/storage/variables.tf +12 -0
  59. package/_skeletons/sibling-template/services/api/pyproject.toml +69 -0
  60. package/_skeletons/sibling-template/services/api/src/api/__init__.py +0 -0
  61. package/_skeletons/sibling-template/services/api/src/api/config.py +31 -0
  62. package/_skeletons/sibling-template/services/api/src/api/core_client.py +64 -0
  63. package/_skeletons/sibling-template/services/api/src/api/main.py +43 -0
  64. package/_skeletons/sibling-template/services/api/src/api/middleware/__init__.py +0 -0
  65. package/_skeletons/sibling-template/services/api/src/api/middleware/auth.py +111 -0
  66. package/_skeletons/sibling-template/services/api/src/api/routers/__init__.py +0 -0
  67. package/_skeletons/sibling-template/services/api/src/api/routers/whoami.py +21 -0
  68. package/_skeletons/sibling-template/services/api/tests/conftest.py +24 -0
  69. package/_skeletons/sibling-template/services/api/tests/test_whoami.py +21 -0
  70. package/_skeletons/sibling-template/services/api/uv.lock +1160 -0
  71. package/core.version +1 -1
  72. package/dist/index.js +113 -87
  73. 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
+ }