diff --git a/.github/scripts/wait-for-image-published.sh b/.github/scripts/wait-for-image-published.sh deleted file mode 100755 index 880a432f9..000000000 --- a/.github/scripts/wait-for-image-published.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -# Wait until the infrastructure repository_dispatch run for this image+tag+sha -# completes successfully. A cancelled run is not a product failure: two image -# publishes can overlap and GitHub then cancels one host job. Re-dispatch once -# and keep polling for a successful run of the same image:tag:sha. -# -# The product Deploy job stays in progress so a develop→main PR shows the -# live deploy result on the same commit, not only the image push. -# -# Required env: GH_TOKEN, DISPATCH_REPO, GITHUB_SHA, IMAGE, TAG, DISPATCHED_AT -# Optional: WAIT_TIMEOUT_SEC (default 1200), WAIT_POLL_SEC (default 10) -set -euo pipefail - -repo="${DISPATCH_REPO:?DISPATCH_REPO is required}" -sha="${GITHUB_SHA:?GITHUB_SHA is required}" -image="${IMAGE:?IMAGE is required}" -tag="${TAG:?TAG is required}" -dispatched_at="${DISPATCHED_AT:?DISPATCHED_AT is required}" -timeout_sec="${WAIT_TIMEOUT_SEC:-1200}" -poll_sec="${WAIT_POLL_SEC:-10}" - -if [ "${#sha}" -ne 40 ]; then - echo "::error::GITHUB_SHA must be a 40-character commit SHA" - exit 1 -fi - -needle="image-published ${image}:${tag} ${sha}" -echo "Waiting for infrastructure run titled: ${needle}" - -deadline=$((SECONDS + timeout_sec)) -run_id="" -redispatched=0 -# Space-separated databaseIds of cancelled runs we already handled. -skipped_ids="" - -find_run() { - json="$(gh run list --repo "$repo" --event repository_dispatch --limit 30 \ - --json databaseId,displayTitle,status,conclusion,createdAt)" - printf '%s\n' "$json" | jq -r --arg n "$needle" --arg t "$dispatched_at" --arg s "$skipped_ids" \ - '[.[] | (.databaseId | tostring) as $id - | select(.displayTitle == $n and .createdAt >= $t - and ($s == "" or (($s | split(" ") | index($id)) == null)))] - | sort_by(.createdAt) | reverse | .[0].databaseId // empty' -} - -poll_run() { - gh run view "$run_id" --repo "$repo" --json status,conclusion -} - -redispatch() { - echo "Re-dispatching image-published ${image}:${tag} ${sha}" - dispatched_at="$(date -u -d '5 seconds ago' +%Y-%m-%dT%H:%M:%SZ)" - gh api "repos/${repo}/dispatches" \ - -f event_type=image-published \ - -f "client_payload[image]=${image}" \ - -f "client_payload[tag]=${tag}" \ - -f "client_payload[sha]=${sha}" - redispatched=1 - run_id="" -} - -while [ "$SECONDS" -lt "$deadline" ]; do - if [ -z "$run_id" ]; then - run_id="$(find_run)" - if [ -z "$run_id" ]; then - echo "No matching infrastructure run yet" - sleep "$poll_sec" - continue - fi - echo "Pinned infrastructure run ${run_id}" - fi - view="$(poll_run)" - status="$(printf '%s\n' "$view" | jq -r '.status')" - conclusion="$(printf '%s\n' "$view" | jq -r '.conclusion // ""')" - if [ "$status" = "completed" ]; then - if [ "$conclusion" = "success" ]; then - echo "Infrastructure deploy succeeded" - exit 0 - fi - echo "Infrastructure run ${run_id} ended (${conclusion})" - if [ "$conclusion" = "cancelled" ] && [ "$redispatched" -eq 0 ]; then - skipped_ids="${skipped_ids} ${run_id}" - redispatch - sleep "$poll_sec" - continue - fi - echo "::error::Infrastructure deploy did not succeed (conclusion=${conclusion})" - exit 1 - fi - echo "Infrastructure run ${run_id} is ${status}" - sleep "$poll_sec" -done - -echo "::error::Timed out waiting for infrastructure deploy of this image" -exit 1 diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml deleted file mode 100644 index 7aecb7eda..000000000 --- a/.github/workflows/deploy-dev.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Deploy DEV - -on: - push: - branches: [develop] - workflow_dispatch: - -concurrency: - group: deploy-dev - cancel-in-progress: false - -env: - DOCKER_TAGS: opencryptopay/api:beta - -permissions: - contents: read - -jobs: - build-and-deploy: - name: Build and deploy to DEV - runs-on: ubuntu-24.04-arm - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: ${{ env.DOCKER_TAGS }} - platforms: linux/arm64 - cache-from: type=registry,ref=opencryptopay/api:buildcache - cache-to: type=registry,ref=opencryptopay/api:buildcache,mode=max - - - name: Notify infrastructure to pull :beta - env: - GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} - DISPATCH_REPO: ${{ secrets.DISPATCH_REPO }} - run: | - set -euo pipefail - if [ -z "${GH_TOKEN}" ] || [ -z "${DISPATCH_REPO}" ]; then - 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" \ - -f event_type=image-published \ - -f "client_payload[image]=opencryptopay/api" \ - -f "client_payload[tag]=beta" \ - -f "client_payload[sha]=${GITHUB_SHA}" - - - name: Wait for infrastructure deploy - env: - GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} - DISPATCH_REPO: ${{ secrets.DISPATCH_REPO }} - IMAGE: opencryptopay/api - TAG: beta - run: bash .github/scripts/wait-for-image-published.sh diff --git a/.github/workflows/deploy-prd.yaml b/.github/workflows/deploy-prd.yaml deleted file mode 100644 index 0b10f5931..000000000 --- a/.github/workflows/deploy-prd.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Deploy PRD - -on: - push: - branches: [main] - workflow_dispatch: - -concurrency: - group: deploy-prd - cancel-in-progress: false - -env: - DOCKER_TAGS: opencryptopay/api:latest - -permissions: - contents: read - -jobs: - build-and-deploy: - name: Build and deploy to PRD - runs-on: ubuntu-24.04-arm - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - push: true - tags: ${{ env.DOCKER_TAGS }} - platforms: linux/arm64 - cache-from: type=registry,ref=opencryptopay/api:buildcache - cache-to: type=registry,ref=opencryptopay/api:buildcache,mode=max - - - name: Notify infrastructure to pull :latest - env: - GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} - DISPATCH_REPO: ${{ secrets.DISPATCH_REPO }} - run: | - set -euo pipefail - if [ -z "${GH_TOKEN}" ] || [ -z "${DISPATCH_REPO}" ]; then - 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" \ - -f event_type=image-published \ - -f "client_payload[image]=opencryptopay/api" \ - -f "client_payload[tag]=latest" \ - -f "client_payload[sha]=${GITHUB_SHA}" - - - name: Wait for infrastructure deploy - env: - GH_TOKEN: ${{ secrets.DISPATCH_TOKEN }} - DISPATCH_REPO: ${{ secrets.DISPATCH_REPO }} - IMAGE: opencryptopay/api - TAG: latest - run: bash .github/scripts/wait-for-image-published.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0a76bd639..2bb40348f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,15 +4,15 @@ The map API keeps the same gates as the 21.gifts API. ## 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` | +| Branch | Purpose | +| --------- | -------------------------------------------------------- | +| `develop` | Integration branch. Open pull requests here. | +| `main` | Production. Moves only through the release pull request. | - 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. +- A push to `develop` runs CI on that commit. The release pull request shows that same CI run. +- This repository does not publish a container image. +- `auto-release-pr.yaml` opens `Release: develop -> main` when `develop` is ahead of `main`. - Never force-push, and never amend a published commit. ## Commit messages @@ -38,19 +38,4 @@ Do not lower a coverage threshold to land a change. A genuinely unreachable bran | 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 8966a9eb3..25049731e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The process listens on port `3000` unless `PORT` is set. The database file is `d ## 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`. +Pull requests target `develop`. `main` moves only through the release pull request from `develop`. This repository does not publish a container image. ## Checks diff --git a/src/__tests__/map/store.test.ts b/src/__tests__/map/store.test.ts index c41196b33..c15367bdc 100644 --- a/src/__tests__/map/store.test.ts +++ b/src/__tests__/map/store.test.ts @@ -29,7 +29,8 @@ describe('MAP_PLACE_SCHEMA_SQL', () => { describe('MemoryMapPlaceStore', () => { it('inserts once and lists newest first without changing the first row', () => { - const store = new MemoryMapPlaceStore(); + let tick = 0; + const store = new MemoryMapPlaceStore(() => new Date(Date.UTC(2026, 0, 1, 0, 0, tick++))); const first = store.insertIfNew(input); expect(first.created).toBe(true); const second = store.insertIfNew({ ...input, name: 'Other', lat: 1 });