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
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Keep the SHA-pinned GitHub Actions current. Dependabot reads the version
# comment beside each pin (e.g. `# v6`) and opens PRs that bump the pin to a
# newer release's commit SHA — the counterpart to pinning, so the pins don't
# rot. Scoped to actions for now (npm in web/ + cdk/ and cargo in crates/ are
# left out to keep PR volume down); the martin release binary isn't trackable
# here and is bumped by hand in scripts/build-martin-lambda.sh.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
commit-message:
prefix: ci
2 changes: 1 addition & 1 deletion .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
- name: Tag + release
Expand Down
28 changes: 14 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ jobs:
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 10
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: lts/*
cache: pnpm
Expand All @@ -33,22 +33,22 @@ jobs:
rust:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: crates
- run: cargo fmt --manifest-path crates/Cargo.toml --all -- --check
- run: cargo clippy --manifest-path crates/Cargo.toml --all-features -- -D warnings
# web/src/generated/{weather,geojson}.ts are generated (from
# contract.rs and the typed-geojson crate) and biome-formatted
# ('just build types'); rerun the pipeline and fail on drift
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 10
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: lts/*
cache: pnpm
Expand All @@ -63,23 +63,23 @@ jobs:
cdk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
# synth compiles weather-ingest via cargo-lambda-cdk and stages the
# prebuilt martin zip, so this needs the Rust toolchain, cargo-lambda,
# and the martin binary — but no AWS creds: the stack is env-agnostic
# (no fromLookup; ARNs and the hosted-zone id are pinned).
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: crates
- name: Install cargo-lambda
run: pip install cargo-lambda
run: pip install cargo-lambda==1.9.1
- name: Build martin lambda zip
run: scripts/build-martin-lambda.sh build/martin-lambda.zip
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 10
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: lts/*
cache: pnpm
Expand Down
46 changes: 29 additions & 17 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ on:
- '.github/workflows/deploy.yml'
workflow_dispatch:

# Union of what the two halves need: OIDC for AWS, contents for checkout,
# deployments to record the production publish.
# Least privilege at the workflow level — just enough for checkout. Each job
# escalates only what it needs (OIDC on infra/web, deployments on web), so the
# `changes` job never receives an OIDC id-token.
permissions:
id-token: write
contents: read
deployments: write

# Serialize every deploy (both halves share the group) so two quick merges can't
# interleave a later infra deploy with an earlier web publish.
Expand All @@ -31,12 +30,15 @@ concurrency: deploy
jobs:
changes:
runs-on: ubuntu-latest
# Just reads the repo to diff paths — no OIDC, no write.
permissions:
contents: read
outputs:
web: ${{ steps.filter.outputs.web }}
infra: ${{ steps.filter.outputs.infra }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3
id: filter
with:
filters: |
Expand All @@ -55,34 +57,38 @@ jobs:
needs: changes
if: needs.changes.outputs.infra == 'true'
runs-on: ubuntu-latest
# OIDC to assume the deploy role + read to checkout. No deployments here.
permissions:
id-token: write
contents: read
# No AWS keys: assumes stormdeck-github-deploy via OIDC (trust-scoped to
# main on this repo). cdk uses the admin bootstrap cfn-exec role.
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: crates
- name: Install cargo-lambda
run: pip install cargo-lambda
run: pip install cargo-lambda==1.9.1

# weather-ingest is compiled by cargo-lambda-cdk during cdk deploy (synth),
# using the cargo-lambda + toolchain above; only the prebuilt martin binary
# needs packaging ahead of time.
- name: Build martin lambda zip
run: scripts/build-martin-lambda.sh build/martin-lambda.zip

- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 10
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: lts/*
cache: pnpm
cache-dependency-path: cdk/pnpm-lock.yaml

- uses: aws-actions/configure-aws-credentials@v6
- uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-2
Expand Down Expand Up @@ -130,9 +136,15 @@ jobs:
needs.infra.result != 'failure' &&
needs.infra.result != 'cancelled'
runs-on: ubuntu-latest
# OIDC to assume the deploy role + read to checkout + deployments to record
# the production publish via the API.
permissions:
id-token: write
contents: read
deployments: write
steps:
# Full history + tags so the version stamp resolves (next step).
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
# Bake in the version this commit ships as: its exact tag if it has one,
Expand All @@ -143,10 +155,10 @@ jobs:
v=$(git describe --tags --exact-match HEAD 2>/dev/null || scripts/next-version.sh patch)
echo "VITE_APP_VERSION=$v" >>"$GITHUB_ENV"
echo "version: $v"
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 10
- uses: actions/setup-node@v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: lts/*
cache: pnpm
Expand All @@ -159,7 +171,7 @@ jobs:
# API_BASE resolves to '' in production builds (web/src/config.ts) —
# same origin as the tiles and weather feeds, so all fetches relative.
BASE_PATH: /
- uses: aws-actions/configure-aws-credentials@v6
- uses: aws-actions/configure-aws-credentials@254c19bd240aabef8777f48595e9d2d7b972184b # v6
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
- name: Create GitHub Release
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ just dev # martin :3030 + vite :5173 (local data, overrides the defau

CDK → CloudFormation: state lives in the account, and pushes to `main` deploy through the repo-pinned OIDC role (the `StormdeckGithubOidc` stack from step 3). `just cdk synth` works offline, and the `profile=` / `region=` variables (`.just/common.just`) thread through every infra recipe (`cdk bootstrap`, `cdk deploy`, `cdk outputs`, `tiles upload`, `weather prime`, …). Module justfiles live in their home folders, so e.g. `just deploy` from inside `cdk/` works too.

Supply chain: every GitHub Action is pinned to a commit SHA (Dependabot bumps them weekly), `cargo-lambda` is version-pinned, and the prebuilt martin binary is fetched from a pinned release and checksum-verified before it's packaged into the Lambda. The deploy workflow grants permissions per job, so only the AWS-touching jobs (`infra`, `web`) ever receive an OIDC token.

One piece lives outside CloudFormation: the stormdeck.live certificate was requested once via the ACM CLI in us-east-1 (CloudFront only takes certs from there) and is pinned by ARN in the stack. Its DNS validation records *are* stack-managed, so renewals stay hands-off. Mind the CAA gotcha: ACM follows CAA policy through CNAMEs, so a record pointing at a host with restrictive CAA (github.io, say) blocks issuance for that name.

## Configuration
Expand Down
45 changes: 37 additions & 8 deletions scripts/build-martin-lambda.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,54 @@
# Package the prebuilt martin release binary as an AWS Lambda zip
# (provided.al2023 / arm64). Martin has native Lambda support: when
# AWS_LAMBDA_RUNTIME_API is set it serves Lambda events instead of HTTP.
# Usage: build-martin-lambda.sh [out.zip] (MARTIN_RELEASE=v1.2.0 to pin)
# Usage: build-martin-lambda.sh [out.zip]
# Downloads a PINNED martin release and verifies its sha256 before baking it
# into the Lambda — "latest" is never used (supply-chain: freeze the exact
# bytes). To bump, update PINNED_RELEASE + PINNED_SHA256 together (get the
# checksum from `shasum -a 256` of the release tarball), or override both
# MARTIN_RELEASE and MARTIN_SHA256 for a one-off. Dependabot can't track
# release binaries, so this is a deliberate manual bump.
set -euo pipefail

OUT="${1:-build/martin-lambda.zip}"
RELEASE="${MARTIN_RELEASE:-latest}"
# Pinned martin release + the sha256 of its aarch64 musl tarball.
PINNED_RELEASE="martin-v1.11.0"
PINNED_SHA256="350692798cbcda7d307adadcd9b16653bfe679fc0d2974ef25e70465255b7bed"

if [ "$RELEASE" = "latest" ]; then
URL="https://github.com/maplibre/martin/releases/latest/download/martin-aarch64-unknown-linux-musl.tar.gz"
else
URL="https://github.com/maplibre/martin/releases/download/${RELEASE}/martin-aarch64-unknown-linux-musl.tar.gz"
OUT="${1:-build/martin-lambda.zip}"
RELEASE="${MARTIN_RELEASE:-$PINNED_RELEASE}"
# The pinned checksum applies only to the pinned release; any override must bring
# its own (MARTIN_SHA256) — we refuse to run an unverified binary.
EXPECTED_SHA256="${MARTIN_SHA256:-}"
if [ -z "$EXPECTED_SHA256" ] && [ "$RELEASE" = "$PINNED_RELEASE" ]; then
EXPECTED_SHA256="$PINNED_SHA256"
fi
if [ -z "$EXPECTED_SHA256" ]; then
echo "error: no sha256 for martin release '$RELEASE' — set MARTIN_SHA256" >&2
exit 1
fi

URL="https://github.com/maplibre/martin/releases/download/${RELEASE}/martin-aarch64-unknown-linux-musl.tar.gz"

mkdir -p "$(dirname "$OUT")"
ABS_OUT="$(cd "$(dirname "$OUT")" && pwd)/$(basename "$OUT")"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT

TARBALL="$WORK/martin.tar.gz"
echo "==> downloading $URL"
curl -fL --progress-bar "$URL" | tar -xz -C "$WORK" martin
curl -fL --progress-bar "$URL" -o "$TARBALL"

# Verify before trusting the contents. shasum -a 256 is on macOS and the CI
# ubuntu runner alike (avoids sha256sum, which macOS lacks).
actual=$(shasum -a 256 "$TARBALL" | cut -d' ' -f1)
if [ "$actual" != "$EXPECTED_SHA256" ]; then
echo "::error::martin tarball sha256 mismatch for $RELEASE" >&2
echo " expected $EXPECTED_SHA256" >&2
echo " actual $actual" >&2
exit 1
fi
echo "==> sha256 verified ($actual)"
tar -xzf "$TARBALL" -C "$WORK" martin

# TILE_SOURCES is a space-separated list of s3:// urls set on the function
# by the stack (unquoted on purpose so it word-splits into one positional
Expand Down