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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Summary

<!-- One or two sentences describing the change. -->

## 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

<!-- Optional context, alternatives considered, follow-ups. -->
69 changes: 69 additions & 0 deletions .github/workflows/auto-release-pr.yaml
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
34 changes: 0 additions & 34 deletions .github/workflows/ci.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .github/workflows/deploy-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-prd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
coverage
bun.lockb
*.lockb
playwright-report
test-results
44 changes: 43 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<tag> <sha>` and fails when that run does not succeed.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading