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
3 changes: 3 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Local development only (wrangler dev). Copy to .dev.vars, which git ignores.
ALLOWED_ORIGINS=https://weather.mukoko.com,http://localhost:3000
PUBLIC_BASE_URL=http://localhost:8787
40 changes: 40 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# EditorConfig — https://editorconfig.org
# Keeps whitespace consistent across editors and IDEs before
# Prettier / rustfmt / ruff get a chance to run.

root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
# Markdown: trailing spaces are significant (hard line-break).
# Let Prettier handle wrapping; don't trim here.
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[*.{json,jsonc}]
indent_size = 2

[*.toml]
indent_size = 4

[Makefile]
indent_style = tab

[*.rs]
# rustfmt handles Rust formatting; EditorConfig just sets the baseline.
indent_size = 4

[*.py]
indent_size = 4

[*.{cpp,cc,c,h,hpp}]
indent_size = 4
70 changes: 70 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Tests, typecheck and a bundle check for the Worker. The org lint gate
# (actionlint, JSON, prettier, markdownlint, yamllint) is lint.yml.
#
# Deploys are not done here: nothing is deployed from this repo until the
# owner provisions the bucket, the DNS record and the route.

name: CI

on:
pull_request:
push:
branches: [main]
merge_group:
workflow_dispatch:

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

permissions:
contents: read

env:
# wrangler needs Node 22 or later.
NODE_VERSION: "24"

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
# The Workers Vitest suite, against stubbed R2 and rate limiter.
- run: npm test

typecheck:
name: Typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
- run: npm run typecheck

build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
# Bundles the Worker exactly as a deploy would, without deploying.
- run: npm run build
145 changes: 145 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Deploys the Worker to Cloudflare from GitHub Actions: on every push to
# `main` (the release merge from `staging`) and by hand. Never on pull
# requests, so no PR can reach the deploy credentials.
#
# The same test, typecheck and bundle checks as ci.yml run first; the deploy
# job only starts when they pass. It runs in the `production` GitHub
# Environment, which holds CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
# Neither value is ever printed.
#
# Until the owner has created the R2 bucket (and put its real name in
# wrangler.jsonc) a deploy fails at the bucket binding: that is intended.
# The maps.nyuchi.com route is NOT enabled in wrangler.jsonc and workers_dev
# is off, so a deployed Worker has no public URL until the route is turned
# on; the smoke test then needs SMOKE_TEST_URL and is skipped without it.

name: Deploy

on:
push:
branches: [main]
workflow_dispatch:

# Never cancel a deploy that is already running.
concurrency:
group: deploy-production
cancel-in-progress: false

permissions:
contents: read

env:
# wrangler needs Node 22 or later.
NODE_VERSION: "24"

jobs:
verify:
name: Verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci
- run: npm test
- run: npm run typecheck
# Bundles the Worker exactly as a deploy would, without deploying.
- run: npm run build

deploy:
name: Deploy
needs: verify
# A manual run from another branch only verifies; it never deploys.
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- run: npm ci

- name: Check Cloudflare credentials are set
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || vars.CLOUDFLARE_ACCOUNT_ID }}
run: |
# Reports only whether each value exists, never the value.
missing=0
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
echo "::error::CLOUDFLARE_API_TOKEN is not set in the production environment"
missing=1
fi
if [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
echo "::error::CLOUDFLARE_ACCOUNT_ID is not set (secret or variable) in the production environment"
missing=1
fi
exit "$missing"

- name: Deploy to Cloudflare
id: deploy
uses: cloudflare/wrangler-action@953926a2e2182532811c01a25e53647d93bf07c0 # v4.1.3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID || vars.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
quiet: "true"

- name: Smoke test
env:
# SMOKE_TEST_URL (a repository or environment variable), for example
# https://maps.nyuchi.com once the route is live, overrides the URL
# wrangler reports.
BASE_URL: ${{ vars.SMOKE_TEST_URL || steps.deploy.outputs.deployment-url }}
run: |
set -euo pipefail
if [ -z "$BASE_URL" ]; then
echo "::warning::No public URL to smoke test (route not enabled, workers_dev off). Set SMOKE_TEST_URL once the route is live."
exit 0
fi
BASE_URL="${BASE_URL%/}"
echo "Smoke testing ${BASE_URL}/health"

# A fresh deploy can take a moment to be served everywhere.
status=000
for attempt in 1 2 3 4 5 6; do
status=$(curl --silent --show-error --max-time 15 \
--dump-header headers.txt --output body.json \
--write-out '%{http_code}' "${BASE_URL}/health" || echo 000)
[ "$status" = "200" ] && break
echo "Attempt ${attempt}: HTTP ${status}, retrying in 10s"
sleep 10
done

fail=0
check() {
echo "::error::Smoke test failed: $1"
fail=1
}

[ "$status" = "200" ] || check "GET /health returned HTTP ${status}, expected 200"

if ! grep -qiE '^content-type:[[:space:]]*application/json' headers.txt; then
check "content-type is not application/json"
fi
if ! grep -qiE '^cache-control:[[:space:]]*no-store' headers.txt; then
check "cache-control is not no-store"
fi

# Exact body, so any unexpected extra field fails.
expected='{"status":"ok","service":"nyuchi-maps"}'
actual=$(tr -d '[:space:]' < body.json)
if [ "$actual" != "$expected" ]; then
check "body is not the expected health document"
fi

exit "$fail"
36 changes: 36 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# The org-wide lint gate: the identical caller from nyuchi/.github. Change
# nothing here. It publishes the five contexts the org ruleset requires:
#
# lint / actionlint
# lint / JSON validity
# lint / prettier
# lint / markdownlint
# lint / yamllint
#
# They come from the job being called `lint` and calling a reusable
# workflow ("<caller job> / <called job>"). Do NOT convert this to a matrix.
#
# The reusable workflow is referenced by `@main` exactly as the org's
# canonical caller does (it is org-owned, not a third-party action); every
# third-party action in this repo is pinned to a commit SHA.

name: Lint

on:
pull_request:
push:
branches: [main, master, scaffold]
# Required for repos whose ruleset uses a merge queue.
merge_group:
workflow_dispatch:

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

permissions:
contents: read

jobs:
lint:
uses: nyuchi/.github/.github/workflows/reusable-lint.yml@main
27 changes: 27 additions & 0 deletions .github/workflows/staging-version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Versioning policy (nyuchi/.github#80): every merge into `staging` is
# released as the next PATCH, tagged on the merged commit. Inert until a
# `staging` branch exists.
name: Staging version

on:
push:
branches: [staging]
workflow_dispatch:
inputs:
bump:
description: Override the bump (major is only ever manual).
type: choice
options: [patch, minor, major]
default: patch

permissions:
contents: read

jobs:
version:
if: github.ref == 'refs/heads/staging'
uses: nyuchi/.github/.github/workflows/reusable-staging-release.yml@924e5b4d0983d31379aedbec02b990793f29c792 # main, 2026-10-04
with:
bump: ${{ inputs.bump || '' }}
permissions:
contents: write
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
node_modules/
.wrangler/
.dev.vars*
!.dev.vars.example
dist/
39 changes: 39 additions & 0 deletions .markdownlint.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// markdownlint-cli2 config for nyuchi/.github.
//
// Relaxed defaults that accommodate:
// - Long prose lines in docs (we format for readability, not width)
// - Inline HTML in the org profile (<div align="center">)
// - Files that start with an HTML comment or a <div> instead of an H1
// - Fenced code blocks without a language tag
//
// See https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
{
"default": true,

// Line length — we wrap for readability, not at a fixed column.
"MD013": false,

// Inline HTML — profile/README.md uses <div align="center"> for the
// landing-page layout and that's intentional.
"MD033": false,

// First line must be a top-level heading — profile/README.md opens
// with an HTML comment, which is correct.
"MD041": false,

// Fenced code blocks without a language — acceptable for output
// blocks and plain-text fences.
"MD040": false,

// Duplicate headings are fine as long as they aren't siblings
// (e.g. "## Summary" appears in multiple docs; that's expected).
"MD024": { "siblings_only": true },

// Unordered list indentation — 2 spaces, matching our house style.
"MD007": { "indent": 2 },

// Table-column-style — re-enabled now that Prettier is in CI and
// auto-formats tables. Prettier's table output should satisfy
// MD060's "aligned" style.
"MD060": true,
}
7 changes: 7 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Needed: without it npm's resolver crashes ("Cannot read properties of null
# (reading 'edgesOut')") on the sharp/undici overrides in package.json, because
# @cloudflare/vitest-pool-workers declares peers it cannot satisfy exactly.
# Remove once that package's peer ranges and the overrides line up.
legacy-peer-deps=true
# Wait a week before resolving newly published versions (npm 11.10+).
min-release-age=7
Loading
Loading