diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..64416c568 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## Summary + + + +## Changes + +- + +## Test plan + +- [ ] `bun run typecheck` +- [ ] `bun run lint` +- [ ] `bun run handbook:check` +- [ ] `bun run e2e:check` +- [ ] `bun run test:coverage` (100% threshold respected) +- [ ] `bun run build` +- [ ] `bun run e2e` +- [ ] Manual smoke: `GET /healthz` returns `200 {"ok":true}` + +## Notes + + diff --git a/.github/workflows/auto-release-pr.yaml b/.github/workflows/auto-release-pr.yaml new file mode 100644 index 000000000..d266fbe3b --- /dev/null +++ b/.github/workflows/auto-release-pr.yaml @@ -0,0 +1,69 @@ +name: Auto Release PR + +on: + push: + branches: [develop] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +concurrency: + group: auto-release-pr + cancel-in-progress: false + +jobs: + create-release-pr: + name: Create Release PR + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch main branch + run: git fetch origin main || true + + - name: Check for existing PR + id: check-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PR_COUNT=$(gh pr list --base main --head develop --state open --json number --jq 'length') + echo "pr_exists=$([[ $PR_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT + + - name: Check for differences + id: check-diff + if: steps.check-pr.outputs.pr_exists == 'false' + run: | + if git rev-parse --verify origin/main >/dev/null 2>&1; then + DIFF_COUNT=$(git rev-list --count origin/main..origin/develop) + else + DIFF_COUNT=0 + fi + echo "has_changes=$([[ $DIFF_COUNT -gt 0 ]] && echo 'true' || echo 'false')" >> $GITHUB_OUTPUT + echo "commit_count=$DIFF_COUNT" >> $GITHUB_OUTPUT + + - name: Create Release PR + if: steps.check-pr.outputs.pr_exists == 'false' && steps.check-diff.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMIT_COUNT: ${{ steps.check-diff.outputs.commit_count }} + run: | + printf '%s\n' \ + "## Automatic Release PR" \ + "" \ + "**Commits:** ${COMMIT_COUNT} new commit(s)" \ + "" \ + "- [ ] Review all changes" \ + "- [ ] Verify CI passes" \ + "- [ ] Merge when ready for production" \ + > /tmp/pr-body.md + + gh pr create \ + --base main \ + --head develop \ + --title "Release: develop -> main" \ + --body-file /tmp/pr-body.md diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..7abe78383 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,55 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [develop] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + name: Typecheck, Lint, Handbook, E2E-check, Test (100% coverage), Build, E2E + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.14 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Lint + run: bun run lint + + - name: Handbook completeness + run: bun run handbook:check + + - name: E2E completeness + run: bun run e2e:check + + - name: Test with 100% coverage gate + run: bun run test:coverage + + - name: Build + run: bun run build + + - name: End-to-end HTTP tests + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' + CI: '1' + run: bun run e2e diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 98e22b2f1..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: CI - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - -concurrency: - group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - check: - name: Typecheck, Lint, Handbook, E2E-check, Test, Build, E2E - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.3.14 - - run: bun install --frozen-lockfile - - run: bun run typecheck - - run: bun run lint - - run: bun run handbook:check - - run: bun run e2e:check - - run: bun run test:coverage - - run: bun run build - - run: bun run e2e - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - CI: '1' diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index eed47bb40..7aecb7eda 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -53,6 +53,8 @@ jobs: echo "::error::DISPATCH_TOKEN or DISPATCH_REPO unset — cannot deploy this image" exit 1 fi + # Five-second slack so a run created just before this clock is still + # eligible; the wait script still matches image+tag+sha. dispatched_at="$(date -u -d '5 seconds ago' +%Y-%m-%dT%H:%M:%SZ)" echo "DISPATCHED_AT=${dispatched_at}" >> "$GITHUB_ENV" gh api "repos/${DISPATCH_REPO}/dispatches" \ diff --git a/.github/workflows/deploy-prd.yaml b/.github/workflows/deploy-prd.yaml index 08f4ae0ff..0b10f5931 100644 --- a/.github/workflows/deploy-prd.yaml +++ b/.github/workflows/deploy-prd.yaml @@ -53,6 +53,8 @@ jobs: echo "::error::DISPATCH_TOKEN or DISPATCH_REPO unset — cannot deploy this image" exit 1 fi + # Five-second slack so a run created just before this clock is still + # eligible; the wait script still matches image+tag+sha. dispatched_at="$(date -u -d '5 seconds ago' +%Y-%m-%dT%H:%M:%SZ)" echo "DISPATCHED_AT=${dispatched_at}" >> "$GITHUB_ENV" gh api "repos/${DISPATCH_REPO}/dispatches" \ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..71a85ea45 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +node_modules +dist +coverage +bun.lockb +*.lockb +playwright-report +test-results diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97babf053..0a76bd639 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,13 +2,55 @@ The map API keeps the same gates as the 21.gifts API. -Before a change is ready: +## Branches + +| Branch | Purpose | Image | +| --------- | -------------------------------------------------------- | -------------------------- | +| `develop` | Integration branch. Open pull requests here. | `opencryptopay/api:beta` | +| `main` | Production. Moves only through the release pull request. | `opencryptopay/api:latest` | + +- Land work on `develop` through a feature branch and a pull request. Do not push `develop` or `main` directly. +- A push to `develop` publishes `:beta` for the dev environment and runs CI on that commit. The release pull request shows that same CI run. +- A push to `main` publishes `:latest` for production. +- `auto-release-pr.yaml` opens `Release: develop -> main` when `develop` is ahead of `main`. A normal production publish waits for that pull request. The dev and production workflows also have a manual run, and that run publishes `:beta` or `:latest` for whichever commit you start it on. +- Never force-push, and never amend a published commit. + +## Commit messages + +English, concise, and about what changed. + +## Checks + +Before a change is ready, run the same commands CI runs. Bun is pinned to `1.3.14` in CI. - `bun run typecheck` - `bun run lint` (ESLint and Prettier) - `bun run handbook:check` — every exported function and every HTTP route has a handbook section - `bun run e2e:check` — every exported function and every HTTP route has an end-to-end title or request - `bun run test:coverage` — 100 percent lines, branches, functions, and statements under `src`, except `src/index.ts` +- `bun run build` - `bun run e2e` — HTTP checks against `GET /healthz` and `/map/places` Do not lower a coverage threshold to land a change. A genuinely unreachable branch needs a `v8 ignore` comment that says why. + +## CI / CD + +| Workflow | Trigger | Action | +| ---------------------- | ----------------------------------- | ----------------------------------------------------------------------- | +| `ci.yaml` | Pull request, and push to `develop` | Typecheck, lint, handbook, e2e-check, test (100% coverage), build, e2e | +| `deploy-dev.yaml` | Push to `develop` | Build and push `opencryptopay/api:beta`, then wait for the deploy | +| `deploy-prd.yaml` | Push to `main` | Build and push `opencryptopay/api:latest`, then wait for the deploy | +| `auto-release-pr.yaml` | Push to `develop` | Open the release pull request (`develop` → `main`) when there is a diff | + +Images target `linux/arm64`. + +Deploy workflows need these Actions secrets: + +| Secret | Purpose | +| ----------------- | ----------------------------------------------------- | +| `DOCKER_USERNAME` | Registry username for the image push | +| `DOCKER_PASSWORD` | Registry token for the image push | +| `DISPATCH_TOKEN` | Token that sends `image-published` and reads that run | +| `DISPATCH_REPO` | `owner/repo` that receives `image-published` | + +If `DISPATCH_TOKEN` or `DISPATCH_REPO` is missing, deploy fails after the image push. The job then waits for the infrastructure run titled `image-published opencryptopay/api: ` and fails when that run does not succeed. diff --git a/README.md b/README.md index ab49e3f36..8966a9eb3 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ The process listens on port `3000` unless `PORT` is set. The database file is `d `GET /healthz` returns `{ "ok": true }`. +## Branches + +Pull requests target `develop`. A push there publishes `opencryptopay/api:beta`. `main` publishes `opencryptopay/api:latest` and moves only through the release pull request from `develop`. + ## Checks -`bun run typecheck`, `bun run lint`, `bun run handbook:check`, `bun run e2e:check`, `bun run test:coverage`, and `bun run e2e`. Coverage on `src` is 100 percent. `src/index.ts` is the process entry and is exercised by the HTTP end-to-end run. +`bun run typecheck`, `bun run lint`, `bun run handbook:check`, `bun run e2e:check`, `bun run test:coverage`, `bun run build`, and `bun run e2e`. Coverage on `src` is 100 percent. `src/index.ts` is the process entry and is exercised by the HTTP end-to-end run.