Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
166 changes: 155 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI
name: CI/CD

on:
push:
Expand All @@ -8,30 +8,174 @@ on:
permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
validation:
name: Validation
runs-on: ubuntu-latest
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.base.repo.id != github.event.pull_request.head.repo.id) || github.event_name == 'push' }}
timeout-minutes: 15
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout Code
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98

- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
with:
node-version: "26.13.0"
cache: "npm"

cache: npm
- name: Install Dependencies
id: dependencies
run: npm ci

- name: Lint Code (ESLint)
- name: Check Formatting
if: ${{ !cancelled() && steps.dependencies.outcome == 'success' }}
run: npm run format:check
- name: Lint Code
if: ${{ !cancelled() && steps.dependencies.outcome == 'success' }}
run: npm run lint

- name: Typecheck
if: ${{ !cancelled() && steps.dependencies.outcome == 'success' }}
run: npm run typecheck

- name: Run Tests
run: npm run test:all
if: ${{ !cancelled() && steps.dependencies.outcome == 'success' }}
run: npm test

build:
name: Build API
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
wrangler-version: ${{ steps.wrangler-version.outputs.version }}
steps:
- name: Checkout Code
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
with:
node-version: "26.13.0"
cache: npm
- name: Install Dependencies
run: npm ci
- name: Read Wrangler Version
id: wrangler-version
run: |
node <<'NODE'
const fs = require('node:fs');
const { version } = require('./node_modules/wrangler/package.json');
fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`);
NODE
- name: Build Deployment Artifact
run: npm run build:api
env:
API_PREVIEW_RESOURCES: ${{ vars.API_PREVIEW_RESOURCES }}
- name: Validate Deployment Artifact
run: npm exec -- wrangler deploy --dry-run --config services/api/dist/wrangler.json
- name: Explain Unconfigured Previews
if: github.event_name == 'push' && github.ref_type == 'branch' && github.ref != 'refs/heads/main' && vars.API_PREVIEW_RESOURCES == ''
run: |
echo '::notice::Preview migrations and deployment are skipped until API_PREVIEW_RESOURCES is configured.'
echo '### Preview deployment skipped' >> "$GITHUB_STEP_SUMMARY"
echo 'Configure the API_PREVIEW_RESOURCES repository variable with isolated staging resources to enable branch Previews. Validation and artifact checks still run.' >> "$GITHUB_STEP_SUMMARY"
- name: Upload Deployment Artifact
if: github.event_name == 'push'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
with:
name: api-worker-build
overwrite: true
path: services/api/dist/
if-no-files-found: error
retention-days: 1

migrations:
name: Apply API Migrations
needs: [validation, build]
if: github.event_name == 'push' && github.ref_type == 'branch' && (github.ref == 'refs/heads/main' || vars.API_PREVIEW_RESOURCES != '')
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
deployment: false
concurrency:
group: api-migrations-${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
cancel-in-progress: false
steps:
- name: Checkout Code
uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
with:
node-version: "26.13.0"
cache: npm
- name: Install Dependencies
run: npm ci
- name: Download Deployment Artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
with:
name: api-worker-build
path: services/api/dist/
- name: Apply Migrations
env:
DEPLOY_TARGET: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: |
config=services/api/dist/wrangler.json
if [ "$DEPLOY_TARGET" = preview ]; then
config=services/api/dist/wrangler.preview-migrations.json
if [ ! -f "$config" ]; then
echo '::error::Configure the API_PREVIEW_RESOURCES repository variable before deploying Previews.'
exit 1
fi
fi
for binding in DB_CENTRAL_ALERTS DB_EXTENSIONS; do
npm exec -- wrangler d1 migrations apply "$binding" --remote --config "$config"
done

preview:
name: Cloudflare Preview
needs: [validation, build, migrations]
if: ${{ !cancelled() && github.event_name == 'push' && github.ref_type == 'branch' && github.ref != 'refs/heads/main' && vars.API_PREVIEW_RESOURCES != '' }}
permissions:
contents: read
actions: read
pull-requests: write
uses: FOSSBilling/.workflows/.github/workflows/cloudflare-workers.yml@384ecc11e315c77cf3d6abb947e859ce62b38cf5 # main
with:
operation: deploy
mode: preview
build-result: ${{ needs.build.result }}
checks-result: ${{ needs.validation.result == 'success' && needs.migrations.result == 'success' && 'success' || 'failure' }}
pr-comment: true
pr-base-branch: main
worker-name: api
environment: preview
artifact-name: api-worker-build
artifact-path: services/api/dist/
config-path: services/api/dist/wrangler.json
wrangler-version: ${{ needs.build.outputs.wrangler-version }}
secrets:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}

deploy:
name: Deploy Production
needs: [validation, build, migrations]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: FOSSBilling/.workflows/.github/workflows/cloudflare-workers.yml@384ecc11e315c77cf3d6abb947e859ce62b38cf5 # main
with:
operation: deploy
mode: production
worker-name: api
environment: production
artifact-name: api-worker-build
artifact-path: services/api/dist/
config-path: services/api/dist/wrangler.json
wrangler-version: ${{ needs.build.outputs.wrangler-version }}
secrets:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
23 changes: 23 additions & 0 deletions .github/workflows/cloudflare-events.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Cloudflare Events

on:
workflow_run:
workflows: ["CI/CD"]
types: [completed]
pull_request_target:
branches: [main]
types: [opened, reopened, synchronize]

permissions:
contents: read
actions: read
pull-requests: write

jobs:
comment:
uses: FOSSBilling/.workflows/.github/workflows/cloudflare-workers.yml@384ecc11e315c77cf3d6abb947e859ce62b38cf5 # main
with:
operation: sync-pr-comment
build-workflow: ci.yml
pr-base-branch: main
worker-name: api
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# prod
dist/
**/dist/

# dev
.vscode/*

# deps
node_modules/
.netlify/
.wrangler

# env
Expand All @@ -20,7 +21,7 @@ logs/
npm-debug.log*

# test coverage
coverage/
**/coverage/

# misc
.DS_Store
3 changes: 1 addition & 2 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
# Ignore workerd generated files
worker-configuration.d.ts
**/worker-configuration.d.ts
85 changes: 13 additions & 72 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,15 @@
# Repository Guidelines

## Project Structure & Module Organization

- `src/app/index.ts` is the worker entrypoint and route wiring for Hono.
- Feature logic lives in `src/services/` and should stay runtime-agnostic.
- Platform interfaces and adapters are in `src/lib/` with Cloudflare and Node implementations under `src/lib/adapters/`.
- Tests mirror the source layout under `test/`, with shared helpers in `test/utils/` and mocks in `test/mocks/`.
- Runtime/config files include `wrangler.jsonc`, `worker-configuration.d.ts`, `tsconfig.json`, `eslint.config.ts`, and `prettier.config.ts`.

### Service layout

Small services are flat: `index.ts` + `interfaces.ts` plus narrow helpers (for example `database.ts`, `db/`, `r2.ts`).

`extensions/v2` is the reference layout for anything larger, and new services should grow into it rather than inventing a third shape:

- `index.ts` — app assembly only: middleware, route registration, OpenAPI document. Route registration order is load-bearing where static paths must beat parameter paths; those cases carry comments.
- `middleware.ts` — service-specific Hono middleware.
- `routes/` — one module per route group, each exporting `register*Routes(app)`. `routes/errors.ts` maps domain error codes to HTTP status; `routes/app.ts` holds the typed app alias.
- `db/` — `schema.ts`, `migrations/`, one `*Database` class per workflow, plus `errors.ts` (D1 constraint classification) and `batch.ts`.
- `schemas/` — zod/OpenAPI contract split by domain. There is deliberately **no barrel**: import from `schemas/<domain>` directly so a module's dependencies are visible. This is why `extensions/v2` has no `interfaces.ts`.
- `github/` — outbound GitHub calls, kept out of the persistence modules.
- `email/` (extensions v2) — provider abstraction and templates. Service-root helpers (`revalidate.ts`, `resource-limits.ts`) stay at the service root. `previews/v1` follows the same split (`routes/`, `github/`, `schemas/`, plus `budget.ts` / `cache.ts`).

Route modules import `getExtensionsDb`/`getAuth`/`getPlatform` and middleware directly. There is no dependency-injection container; tests drive the real app through `app.request`.

Each service documents its own contract and operational detail in its own `README.md` (`src/services/<name>/<version>/README.md`). Keep API behaviour there rather than here or in the root README: this file is for conventions that apply when modifying the code.

## Build, Test, and Development Commands

- `npm install`: install dependencies.
- `npm run dev`: start the local Cloudflare Workers dev server.
- `npm run cf-deploy`: apply remote D1 migrations, then deploy via Wrangler.
- `npm run cf-typegen`: regenerate `worker-configuration.d.ts`.
- `npm run db:migrate:all:local` / `db:migrate:all:remote`: apply all D1 migrations locally / remotely.
- `npm run test`: run the default Vitest suite (Workers pool).
- `npm run test:all`: run Workers pool plus Node adapter tests.
- `npm run test:node`: run Node adapter tests via `vitest.node.config.ts`.
- `npm run test:coverage`: run tests with coverage.
- `npm run typecheck`: TypeScript typecheck without emit.
- `npm run lint` / `npm run lint:fix`: lint the codebase (with optional fixes).
- `npm run format` / `npm run format:check`: format or verify formatting with Prettier.

## Coding Style & Naming Conventions

- TypeScript (ES modules), 2-space indentation, and Prettier formatting (`trailingComma: "none"`).
- ESLint uses `@typescript-eslint` with Prettier compatibility; keep lint clean before opening PRs.
- Use kebab-case for service folders (for example `central-alerts`), and name tests `*.test.ts`.

## Testing Guidelines

- Vitest is the primary test runner; integration tests live in `test/integration/`.
- Prefer colocating tests in the matching `test/` path (for example `src/services/versions/v1` -> `test/services/versions/v1`).
- Run `npm run test:all` before release-related changes to cover both worker and node adapters.

## Commit & Pull Request Guidelines

- Recent commits use short, imperative, sentence-case subjects (for example "Update API docs", "Refactor versions service").
- No ticket prefixes are evident; keep messages focused and under ~72 chars.
- PRs should include a concise summary, testing notes (commands run), and call out config/binding changes in `wrangler.jsonc`.

## Configuration & Secrets

- Local secrets go in `.dev.vars` (for example `GITHUB_TOKEN="..."`).
- Bindings, cron, and vars are defined in `wrangler.jsonc` (D1, KV, R2, Durable Objects, rate limiters, service bindings); keep names aligned with `CloudflareBindings`.
- Use Wrangler secrets for production tokens instead of committing them.
- `ASSERTION_SIGNING_SECRET`: HMAC key the extensions v2 API uses to verify short-lived bearer assertions minted by the extensions site (`src/lib/auth/bearer-assertion.ts`). Not sent over the wire — only signs/verifies server-side in each Worker. Add `ASSERTION_SIGNING_SECRET="..."` to `.dev.vars` for local dev; set via `wrangler secret put ASSERTION_SIGNING_SECRET` in production, matching the value configured in the extensions site's Worker.
- `EXTENSIONS_REVALIDATE_SECRET`: bearer token the extensions v2 API sends to the extensions site's `POST /api/revalidate` (over the `EXTENSIONS_FRONTEND` service binding) to purge the site's CDN-cached extension pages after content mutations. Must match the value configured in the extensions site's Worker. Same local/prod setup as `ASSERTION_SIGNING_SECRET`.

## Cache Revalidation

Any endpoint that mutates publicly visible content (revision approve/reject, moderator-correct, delist/relist, developer approve, developer profile upsert via `PUT /developers/me`, profile deletion via `DELETE /developers/me`, claim approve/reject, extension withdraw) must call `revalidateExtensions(c)` from `src/services/extensions/v2/revalidate.ts` after a successful write.

Skipping it does not break correctness — the site's `maxAge`/`stale-while-revalidate` windows bound staleness — but changes then take minutes instead of seconds to appear. Profile deletion keeps its purge even though the `developers` tag is currently a no-op.
This is an npm workspace repository. Deployable components live under `services/`;
reusable libraries live under `libraries/`. Service-specific instructions live in
`services/api/AGENTS.md`.

- Install dependencies with `npm ci` from the repository root. Keep one root lockfile.
- Services must not import each other's source. Put code shared by actual callers in a private workspace package.
- Declare runtime dependencies in the workspace that uses them. Shared development tools are at the root.
- Preserve the API's existing feature structure inside `services/api/src/services/`.
- Keep provider entry points thin; domain and processing logic should remain portable.
- Local secrets and runtime state belong beside their service (`.dev.vars`, `.wrangler/`). Never commit credentials.
- Run `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm test`, and `npm run build:api` before submitting changes.
- `npm run build:api` is a local dry run. `npm run cf-deploy` applies remote migrations and deploys production.
- Keep commits focused and use short imperative subjects. PRs should explain behaviour, validation and any deployment setup required.
Loading
Loading