A reusable workflow crawls a named city on whatever schedule its caller sets, gates on the bundle's own versioned filename -- which embeds the capture day and a stamp over everything inside, so data changes, tool changes and policy revisions all read as change -- and publishes only when the city moved. State is a git branch holding the archive: durable at any cadence, diffable history for free. Exported zone reports on the same branch opt a city into rules enrichment by their presence; private curation rides there too, adopted into the build without ever being published. The repository schedules its own proof cities weekly as the living test; docs/snapshots.md is the recipe a city IT team copies into a private repository, licensing note included.
- commit
- 53883d05201882dca238c5caf388db3735b22a44
- parent
- dacc652f0846f103972313e65f43259ca97382c4
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 0.5 of 3, Jev's estimate
| A | .github/ | 127 | ++++++++++++++++++++++++++++++ |
| A | .github/ | 22 | +++++ |
| A | docs/ | 84 | ++++++++++++++++++++ |
@@ -0,0 +1,127 @@
1+# Snapshot builds one city's .atlas on a schedule and publishes it only when2+# the city changed. It is reusable: a caller names the city and provides the3+# state -- an fmg-archive committed to a branch of the calling repository --4+# and this workflow crawls, composes, gates on the bundle's own content5+# stamp, and publishes to the caller's releases. A city IT team runs their6+# own copy in a private repository with their curation and their key; this7+# repository's scheduler runs it over the public proof cities as the living8+# test of the same path.9+name: Snapshot10+11+on:12+ workflow_call:13+ inputs:14+ city:15+ description: "curated city slug, e.g. bend-or"16+ required: true17+ type: string18+ atlas-ref:19+ description: "atlas ref to build with"20+ required: false21+ type: string22+ default: main23+ state-branch:24+ description: "branch of the calling repository holding the fmg-archive"25+ required: false26+ type: string27+ default: snapshots28+29+permissions:30+ contents: write31+32+env:33+ CGO_ENABLED: "0"34+ GOFLAGS: -trimpath -buildvcs=false35+36+jobs:37+ snapshot:38+ runs-on: ubuntu-24.0439+ steps:40+ - name: Check out snapshot state41+ uses: actions/checkout@v442+ with:43+ ref: ${{ inputs.state-branch }}44+ path: state45+ # The state branch must exist; docs/snapshots.md shows the one-time46+ # orphan-branch setup.47+48+ - name: Check out atlas49+ uses: actions/checkout@v450+ with:51+ repository: FelineStateMachine/atlas52+ ref: ${{ inputs.atlas-ref }}53+ path: atlas54+55+ - name: Adopt private curation56+ run: |57+ if [ -f state/cities_local.go ]; then58+ cp state/cities_local.go atlas/internal/arcgismap/cities_local.go59+ echo "private curation adopted"60+ fi61+62+ - uses: actions/setup-go@v563+ with:64+ go-version-file: atlas/go.mod65+ cache-dependency-path: atlas/go.sum66+67+ - name: Crawl68+ working-directory: atlas69+ run: |70+ # Zone reports exported from a Zoneomics subscription live beside71+ # the archive on the state branch, one directory per city; a72+ # directory's presence is the opt-in.73+ FLAGS=""74+ if [ -d "../state/zoneomics/${{ inputs.city }}" ]; then75+ FLAGS="-zoneomics ../state/zoneomics/${{ inputs.city }}"76+ fi77+ go run ./tools/crawl -archive ../state/fmg-archive -arcgis "${{ inputs.city }}" $FLAGS78+79+ - name: Compose80+ working-directory: atlas81+ run: |82+ go run ./tools/tiles -source ../state/fmg-archive -output build/tiles83+ go run ./tools/generate -source ../state -tiles build/tiles/index.json -bundles ../out84+85+ - name: Gate on the stamp86+ id: gate87+ working-directory: out88+ env:89+ GH_TOKEN: ${{ github.token }}90+ run: |91+ FILE=$(ls ${{ inputs.city }}-*.atlas | sort | tail -1)92+ echo "file=$FILE" >> "$GITHUB_OUTPUT"93+ if gh release view "snapshot-${{ inputs.city }}" --repo "$GITHUB_REPOSITORY" \94+ --json assets -q '.assets[].name' 2>/dev/null | grep -qx "$FILE"; then95+ echo "unchanged: $FILE is already published"96+ echo "changed=false" >> "$GITHUB_OUTPUT"97+ else98+ echo "changed=true" >> "$GITHUB_OUTPUT"99+ fi100+101+ - name: Commit the archive102+ if: steps.gate.outputs.changed == 'true'103+ working-directory: state104+ run: |105+ git config user.name "atlas-snapshot"106+ git config user.email "snapshot@users.noreply.github.com"107+ git add -A108+ git diff --cached --quiet || git commit -m "snapshot ${{ inputs.city }}: ${{ steps.gate.outputs.file }}"109+ git push110+111+ - name: Publish112+ if: steps.gate.outputs.changed == 'true'113+ env:114+ GH_TOKEN: ${{ github.token }}115+ run: |116+ FILE="${{ steps.gate.outputs.file }}"117+ RELEASE="snapshot-${{ inputs.city }}"118+ if ! gh release view "$RELEASE" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then119+ gh release create "$RELEASE" --repo "$GITHUB_REPOSITORY" \120+ --title "Snapshots: ${{ inputs.city }}" \121+ --notes "Scheduled .atlas snapshots. Every published version stays; *-latest.atlas always names the newest." \122+ --target "${{ inputs.state-branch }}"123+ fi124+ cp "out/$FILE" "out/${{ inputs.city }}-latest.atlas"125+ gh release upload "$RELEASE" --repo "$GITHUB_REPOSITORY" \126+ "out/$FILE" "out/${{ inputs.city }}-latest.atlas#${{ inputs.city }}-latest.atlas" --clobber127+ echo "published $FILE"@@ -0,0 +1,22 @@
1+# The scheduler over the public proof cities: the living test of the same2+# reusable workflow a city IT team points at their own city with their own3+# zone reports; see docs/snapshots.md for that setup.4+name: Snapshots5+6+on:7+ schedule:8+ - cron: "17 6 * * 1"9+ workflow_dispatch:10+11+permissions:12+ contents: write13+14+jobs:15+ city:16+ strategy:17+ fail-fast: false18+ matrix:19+ city: [bend-or, redondo-beach-ca]20+ uses: ./.github/workflows/snapshot.yml21+ with:22+ city: ${{ matrix.city }}@@ -0,0 +1,84 @@
1+# Scheduled city snapshots2+3+A city's `.atlas` can build itself on a schedule: crawl the city's ArcGIS4+hub (and, with a key, the Zoneomics rules behind its zoning), exit quietly5+when nothing changed, and publish a new versioned bundle when something did.6+The reusable workflow lives in this repository7+(`.github/workflows/snapshot.yml`); this repository's own scheduler runs it8+weekly over the public proof cities. A city IT team runs the same workflow9+in a repository of their own, so every employee works off the same enriched10+`.atlas` at whatever cadence the team chooses.11+12+## What a run does13+14+1. Crawls the city into the `fmg-archive` committed on the state branch.15+ Captures are content-addressed: an unchanged city registers nothing.16+2. Composes the bundle. Its filename embeds the capture day and a content17+ stamp over everything inside.18+3. Gates: if that exact filename is already a release asset, the run ends —19+ "unchanged". The gate catches data changes, tool changes, and policy20+ revisions alike, because the stamp covers them all.21+4. Otherwise commits the archive delta (durable, diffable history at any22+ cadence — weekly, monthly, quarterly) and uploads the bundle to the23+ rolling `snapshot-<city>` release. Every published version stays as an24+ asset; `<city>-latest.atlas` always names the newest, so one URL serves25+ the whole team.26+27+## Setting it up in your own repository28+29+One-time, in a (typically private) repository:30+31+```sh32+git switch --orphan snapshots33+# your city's curation, if it is not one of the public proof cities:34+cp /path/to/cities_local.go .35+git add -A && git commit -m "snapshot state"36+git push -u origin snapshots37+git switch main38+```39+40+Then a workflow:41+42+```yaml43+name: City snapshot44+on:45+ schedule:46+ - cron: "0 6 * * 1" # weekly; monthly/quarterly are equally safe47+ workflow_dispatch:48+permissions:49+ contents: write50+jobs:51+ snapshot:52+ uses: FelineStateMachine/atlas/.github/workflows/snapshot.yml@main53+ with:54+ city: your-city-slug55+```56+57+The `cities_local.go` on the state branch is adopted into the build, so a58+private city's curation never has to be published. Employees fetch59+`https://github.com/<org>/<repo>/releases/download/snapshot-<city>/<city>-latest.atlas`60+and drop it into their Atlas bundles directory — or IT mirrors it onto a61+share the app watches.62+63+## Zoneomics, plainly64+65+Zoning-rule enrichment comes from **zone reports you export from your own66+Zoneomics subscription** — the attribute/value CSVs the platform produces,67+one per zoning district. Place them on the state branch under68+`zoneomics/<city-slug>/` and every snapshot joins them to the city's zoning69+zones by zone code; each district's card then carries its purpose,70+permitted and prohibited uses, and dimensional standards, offline.71+72+- No API and no scraping: the API prices single-point reports and the73+ public code pages bar automated access, so neither fits correlating a74+ whole town. Files you exported under your own subscription are the75+ honest channel, and a snapshot run needs no key and no wire.76+- Reports are *point* reports: below the district rules they describe the77+ queried parcel — its owner's name, address, valuation. The importer78+ drops those rows unconditionally; they never enter a capture.79+- Enrichment is all-or-nothing per capture: an unreadable report fails the80+ run rather than silently minting a version that lost its rules.81+- **Before an organization circulates enriched bundles internally, confirm82+ that its Zoneomics subscription permits embedding and internal83+ redistribution of exported report content.** Without reports, snapshots84+ are pure open data with no strings attached.