-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMakefile
More file actions
801 lines (755 loc) · 35.6 KB
/
Copy pathMakefile
File metadata and controls
801 lines (755 loc) · 35.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
.PHONY: \
build watch i18n labels templates format format-check cli-build cli-check \
audit-plan mcp mcp-bundle mcp-call mcp-serve mcp-test release patch publish \
build-web build-web-js upload-symbols-web strip-source-maps-web release-web \
release-android patch-android \
release-ios patch-ios \
release-tag release-notes \
require-clean-tree \
netlify-dev netlify-dev-catalog-seed-all site-dev catalog-seed catalog-seed-demos catalog-feed catalog-reset
.SILENT: \
build watch i18n labels templates format format-check \
cli-build cli-check audit-plan mcp mcp-bundle mcp-call mcp-serve mcp-test release patch
# Local Netlify dev configuration. Override on the command line, e.g.:
# make catalog-seed SEED_DRILL=path/to/other.drill
LOCAL_BASE_URL ?= http://localhost:8888
LOCAL_ADMIN_TOKEN ?= dev-token
SEED_DRILL ?= test/fixtures/test-7x.drill
# `netlify functions:serve` (see the netlify-dev target below) does not
# apply netlify.toml's /api/* redirects, so the CLI's default
# `functions-base-path` of "/api" 404s against it. Point the CLI at the
# native `/.netlify/functions/*` path instead for every seed/feed target
# that talks to a locally-served backend.
LOCAL_FUNCTIONS_BASE_PATH ?= /.netlify/functions
# Git commit metadata injected into builds via --dart-define. The values
# get baked into the binary (see lib/utils/app_build_info.dart), surface
# on the About page and ship as a `commit` tag with every Sentry event,
# so a Sentry report can be traced back to the exact source tree even
# when the SemVer build number is reused across patches.
#
# `:=` (not `=`) forces a single eager shell-out: make evaluates this
# once at parse time instead of every reference, which matters because
# `git rev-parse` is not free. The `|| echo unknown` keeps things
# building when the source is extracted outside of a git checkout (e.g.
# a release tarball).
#
# `GIT_DIRTY` appends a `-dirty` suffix when the working tree has
# uncommitted changes. That way a build cut from a half-committed
# workspace can never be confused with the pristine commit on GitHub.
GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo unknown)
GIT_COMMIT_SHORT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
GIT_DIRTY := $(shell git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null || echo -dirty)
DART_DEFINE_GIT := --dart-define=GIT_COMMIT=$(GIT_COMMIT)$(GIT_DIRTY) --dart-define=GIT_COMMIT_SHORT=$(GIT_COMMIT_SHORT)$(GIT_DIRTY)
# Migration kill-switch for ADR-0039 Phase 1. When set to `true` the in-app
# migration banner, drawer entry and explainer are completely hidden. Used
# while Phase 1 is on apex but `web.ringdrill.app` (Phase 2) has not yet
# been stood up, so users do not see a banner pointing at a domain that
# does not resolve. Default empty leaves the host detection in
# lib/web/legacy_host_web.dart in charge.
MIGRATION_DISABLED ?=
DART_DEFINE_MIGRATION := $(if $(MIGRATION_DISABLED),--dart-define=MIGRATION_DISABLED=$(MIGRATION_DISABLED),)
build: labels templates
echo "Run code generation..."
dart run build_runner build
watch:
echo "Watch for buildable changes..."
dart run build_runner watch
# Regenerate Flutter localization sources from lib/l10n/app_*.arb.
# `make build` only covers freezed/json_serializable; the gen-l10n
# step is a separate Flutter tool and must be run after any ARB
# change. The generated `app_localizations*.dart` files must never
# be hand-edited (see CLAUDE.md).
#
# Depends on `labels` because both read the same ARBs: gen-l10n produces the
# Flutter-bound AppLocalizations, `labels` the Flutter-free copy the CLI needs.
# An ARB change has to reach both or the two disagree.
i18n: labels
echo "Generate Flutter localizations from ARB..."
flutter gen-l10n
# Regenerate the Flutter-free subset of ARB messages that the CLI's `build` and
# `render` need (DESIGN-014, the ADR-0048 amendment). Cheap and deterministic:
# no ARB change means no diff, so it is safe as a prerequisite of both `build`
# and `i18n` rather than something to remember. test/l10n/
# headless_labels_sync_test.dart is the backstop if it is skipped anyway.
labels:
echo "Generate headless ARB labels..."
dart run tools/generate_headless_labels.dart
# Bake assets/templates/*.mustache into Dart, for the same reason as `labels`:
# BriefRenderer used to load them through the Flutter asset bundle, which the
# CLI's `render` cannot reach (DESIGN-014, the ADR-0048 amendment). Reading from
# disk would work under `dart run` but not from an installed CLI, which has no
# assets/ directory beside it. test/services/brief/brief_templates_sync_test.dart
# is the backstop if this is skipped.
templates:
echo "Bake brief templates into Dart..."
dart run tools/generate_brief_templates.dart
# Apply Dart's default formatting across the tree.
#
# `lib/` and `test/` are formatted baselines, so a `git diff` after this target
# is your own change and nothing else. That was not always true: while the
# baseline had drifted, running the formatter to tidy one file reformatted dozens
# of unrelated ones, which then had to be reverted out of each commit to keep it
# scoped. Keep it that way — see AGENTS.md rule 10.
format:
echo "Format Dart sources..."
dart format lib/ test/ bin/
# Fails when anything is unformatted, without writing. For a pre-commit hook or
# a CI gate.
format-check:
echo "Check Dart formatting..."
dart format --output=none --set-exit-if-changed lib/ test/ bin/
# Enforce AGENTS.md rule 7 / ADR-0005: the CLI must stay free of Flutter.
#
# Building it with the *Dart* SDK is the check that actually bites — a stray
# `package:flutter/*` in the import closure fails to resolve `dart:ui` here,
# while `flutter analyze` accepts it happily and `dart pub global activate` only
# breaks later, on someone else's machine. Worth wiring up now because
# DESIGN-014 grew that closure a long way past the two files it used to be
# (drill_client + drill_file): it now reaches the source compiler, the models,
# the schedule derivation and the headless labels.
#
# `dart build cli` rather than `dart compile exe`: a transitive dependency
# (objective_c, via the notification/geolocator plugins) ships build hooks, and
# `dart compile` refuses to run those. Output lands in build/cli/, which is
# gitignored.
#
# test/bin/cli_flutter_free_test.dart is the fast counterpart — it walks the same
# closure in milliseconds and names the offending import chain, which a link
# error does not.
cli-check: cli-build
echo " ok — bin/ringdrill.dart has no Flutter in its import closure"
# The build itself, factored out so `mcp` can depend on it: the MCP server runs
# the CLI once per tool call, and a compiled binary is ~0.6s against ~2.9s for
# `dart run` — which `get_plan` pays twice.
cli-build:
echo "Build the CLI with the Dart SDK..."
dart build cli
# ---------------------------------------------------------------------------
# MCP server (DESIGN-014 stage 4) — local development
# ---------------------------------------------------------------------------
# Get set up to test the MCP server: build the CLI it shells out to, then print
# the client configuration to paste in. The server finds the built binary by
# itself, so there is nothing to configure beyond this.
mcp: cli-build
echo
echo "MCP server ready. Add this to your client's config:"
echo
echo ' {'
echo ' "mcpServers": {'
echo ' "ringdrill": {'
echo ' "command": "node",'
echo ' "args": ["$(CURDIR)/mcp/ringdrill-mcp.mjs"]'
echo ' }'
echo ' }'
echo ' }'
echo
echo "Try a tool without a client: make mcp-call ARGS='schema'"
echo "List the tools: make mcp-call"
# Cross-compile the source compiler to JavaScript for the hosted MCP endpoint
# (ADR-0060). The output is committed: a Netlify build has no Dart SDK, the same
# reason headless_labels.g.dart and brief_templates.g.dart are committed.
#
# -O2 minifies, which roughly halves the bundle over the default. Anything above
# -O2 enables assumptions (omitting implicit downcasts) that would trade a clear
# failure for a silent wrong answer in code that computes coordinates and a hash.
mcp-bundle:
echo "Cross-compile the source compiler to JavaScript..."
dart compile js tools/mcp_js_entry.dart -O2 \
-o netlify/functions/lib/mcp-compiler-bundle.js
# .deps is dart2js's own list of every file it compiled, which is the only
# drift-free answer to "what is this bundle built from" — so it becomes the
# committed stamp the staleness check compares content against, before it is
# thrown away. The alternative, a hand-listed set of source roots, was both
# too wide and too narrow: it watched lib/l10n/app_localizations.dart, which
# is Flutter code the bundle never reaches.
node tools/mcp-bundle-stamp.mjs write \
netlify/functions/lib/mcp-compiler-bundle.js.deps
# .deps and .map are by-products. The map is deliberately not committed:
# it is 3x the bundle, only helps a server-side stack trace, and this repo
# already strips maps before publishing (strip-source-maps-web) rather than
# shipping its sources.
rm -f netlify/functions/lib/mcp-compiler-bundle.js.deps
rm -f netlify/functions/lib/mcp-compiler-bundle.js.map
echo " wrote netlify/functions/lib/mcp-compiler-bundle.js"
# One-shot tool call, for poking at the server by hand. See mcp/dev-call.mjs for
# the argument syntax (key=value, @file to read a file, --raw for the payload).
# make mcp-call ARGS='create_plan name="LSOR 2027" teams=4 --raw'
mcp-call:
node mcp/dev-call.mjs $(ARGS)
# Audits a source document for what `analyze` cannot see: the three fields the app
# asks for by name, derived values typed into prose, and literals that want to be
# variables. Exits 1 on a missing expected field, so it can gate an authoring loop.
#
# make audit-plan PLAN=path/to/plan.yaml
# make audit-plan PLAN=after.yaml BASELINE=before.yaml
#
# The BASELINE form is the point: it makes "did that instruction change help" a diff
# rather than an impression.
audit-plan:
dart run tools/audit_authoring.dart $(PLAN) $(if $(BASELINE),--baseline=$(BASELINE),)
# The server's own tests. Drive it over a real stdio pipe, deliberately via
# `dart run` so the build-hooks preamble it has to tolerate stays exercised.
mcp-test:
node --test mcp/tests/*.test.mjs
# Serve the hosted endpoint (ADR-0060) locally, so it can be exercised over real
# HTTP rather than only through its handler. Needs the bundle, and worth using
# rather than trusting the unit tests: Netlify's esbuild step transforms what it
# imports, and that is how the dart2js/rti breakage was found.
#
# curl -s localhost:8888/.netlify/functions/mcp -H 'content-type: application/json' \
# -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
mcp-serve: mcp-bundle
echo "Serving the hosted MCP endpoint at http://localhost:8888/.netlify/functions/mcp"
npx netlify functions:serve --port 8888
# Web release pipeline. Decomposed so CI can run the steps individually
# (one log group per step) but `make release-web` is the one-shot used
# locally and as a sanity check.
#
# Why source maps live on disk between build-web and strip-source-maps-web:
# sentry_dart_plugin needs the .map files next to main.dart.js so it can
# resolve the original Dart sources. They are stripped from build/web/
# AFTER upload so they never reach the public CDN — serving them would
# expose the unminified source to anyone who opens DevTools.
# --wasm produces both dart2wasm and dart2js outputs. The
# boot loader picks dart2wasm when the browser supports WASM
# GC (Chrome 119+, Firefox 120+, Safari 18.2+) and falls back
# to dart2js otherwise, so iOS 17 and older users see no
# regression. Expected gain: TBT down ~50-70%, TTI down
# ~30-50%, Performance score up 10-20 points.
#
# Bundle is ~15-25% larger because both compilations ship; the
# CDN serves only one variant per request based on the
# browser's capability headers.
#
# If a WASM-related regression shows up in production, fall
# back to `build-web-js` (dart2js only) until the issue is
# diagnosed. Same release-web wiring works for either target.
build-web:
flutter build web \
--wasm \
--release \
--pwa-strategy=offline-first \
--source-maps \
$(DART_DEFINE_GIT) \
$(DART_DEFINE_MIGRATION)
mkdir -p build/web/.well-known
cp -f web/.well-known/assetlinks.json build/web/.well-known/assetlinks.json
cp -f web/_headers build/web/_headers
# dart2js-only fallback. Kept around so we can bisect WASM
# regressions without reverting commits, and so we have a
# known-good path if dart2wasm breaks for some plugin update.
# Drops to roughly the bundle size and runtime characteristics
# we had pre-WASM. Swap into release-web by hand:
# make build-web-js upload-symbols-web strip-source-maps-web
build-web-js:
flutter build web \
--release \
--pwa-strategy=offline-first \
--source-maps \
$(DART_DEFINE_GIT) \
$(DART_DEFINE_MIGRATION)
mkdir -p build/web/.well-known
cp -f web/.well-known/assetlinks.json build/web/.well-known/assetlinks.json
cp -f web/_headers build/web/_headers
upload-symbols-web:
dart run sentry_dart_plugin
strip-source-maps-web:
find build/web -type f -name '*.js.map' -delete
# Also drop the trailing `//# sourceMappingURL=...` comment from the JS.
# Deleting the .map file alone leaves the reference in main.dart.js, so
# the browser and Sentry's source scraper still try to fetch the now-404
# map and log it as a download error. Removing the comment stops that.
#
# perl, not `sed -i`, because this target runs on both hosts that invoke
# it: CI on ubuntu-latest (GNU sed, where `-i` takes no argument) and a
# maintainer's macOS box (BSD sed, where `-i` takes the backup suffix as a
# separate argument). BSD sed therefore swallowed the script as the suffix
# and tried to parse the first filename as the program, failing with
# "undefined label 'uild/web/flutter_bootstrap.js'". There is no single
# `sed -i` spelling that works on both; perl's -i needs no argument.
#
# The substitution uses `|` delimiters, NOT `s{...}{}`, and that is load
# bearing: GNU find counts every `{}` in the -exec argument list and
# rejects more than one with `+` ("Only one instance of {} is supported"),
# so a brace-delimited perl script breaks CI even though BSD find accepts
# it. Keep exactly one `{}` here — the file-list placeholder.
#
# `-exec ... +` rather than `xargs`: it does not run at all on an empty
# file list, whereas GNU xargs would invoke perl with no files and hang on
# stdin (and BSD xargs has no portable `-r` to prevent that).
#
# `\R?`, not `\R`, so the comment is stripped even when it is the last
# line with no trailing newline.
find build/web -type f -name '*.js' \
-exec perl -pi -e 's|^//\# sourceMappingURL=.*\R?||' {} +
# Refuse to build a release from a working tree that has uncommitted
# changes. Without this gate, $(DART_DEFINE_GIT) would tag the binary
# with `<sha>-dirty`, which:
# - makes the GitHub link on the About page resolve to "404 — commit
# not found" because GitHub does not know about the dirty SHA;
# - leaves Sentry events pointing at a SHA that nobody else can
# reproduce from the public repo.
#
# Run order matters: we want this to fail BEFORE shorebird or flutter
# burns 5+ minutes building. Make does no parallelism between targets
# in a sequential dependency list, so listing it first guarantees the
# gate runs before the heavy build step.
#
# `git status --short` is shown on failure so the developer sees
# exactly which files need to be committed or stashed instead of
# having to re-run `git status` themselves. The hint about
# `ALLOW_DIRTY=1` is intentionally NOT supported — a release with
# untracked code is the exact bug this gate exists to prevent. If a
# one-off escape hatch ever becomes necessary, add it via a dedicated
# target (e.g. `release-android-dirty`) rather than a flag, so it
# leaves a clear trace in CI logs and shell history.
require-clean-tree:
@if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then \
echo "ERROR: refusing to build a release from a dirty working tree."; \
echo "Commit or stash these changes first:"; \
git status --short; \
exit 1; \
fi
release-web: require-clean-tree build-web upload-symbols-web strip-source-maps-web
release-android: require-clean-tree
shorebird release android -- \
--obfuscate \
--split-debug-info=build/debug-info \
$(DART_DEFINE_GIT)
dart run sentry_dart_plugin
patch-android: require-clean-tree
shorebird patch android -- \
--obfuscate \
--split-debug-info=build/debug-info \
$(DART_DEFINE_GIT)
dart run sentry_dart_plugin
# iOS release/patch via Shorebird, mirroring the Android targets above:
# same require-clean-tree gate, same --obfuscate / --split-debug-info /
# git dart-defines, same sentry_dart_plugin run afterwards (uploads the
# iOS dSYMs in addition to Android symbols).
#
# Unlike Android these only run on a macOS host with Xcode, and code
# signing must already be configured in ios/Runner.xcodeproj (DISCOOS
# team, app.ringdrill, automatic signing — see ADR-0021). Shorebird drives
# `flutter build ipa` under the hood and signs with that configuration.
#
# `shorebird release ios` produces build/ios/ipa/*.ipa for App Store
# Connect; `shorebird patch ios` ships a code-push patch to the matching
# released version.
release-ios: require-clean-tree
shorebird release ios -- \
--obfuscate \
--split-debug-info=build/debug-info \
$(DART_DEFINE_GIT)
dart run sentry_dart_plugin
patch-ios: require-clean-tree
shorebird patch ios -- \
--obfuscate \
--split-debug-info=build/debug-info \
$(DART_DEFINE_GIT)
dart run sentry_dart_plugin
# Local backend for development. Uses `netlify functions:serve` (not
# `netlify dev`) because the latter sets up an Edge Functions runtime
# that fails to install reliably on macOS hosts. functions:serve runs the
# Lambda-compat function host directly on $(LOCAL_BASE_URL).
#
# Caveat: redirects in netlify.toml (/api/* and /d/*) do NOT apply here.
# DrillClient defaults to calling /api/*, which 404s against this mode —
# catalog-seed/catalog-seed-demos/catalog-feed below pass
# --functions-base-path (LOCAL_FUNCTIONS_BASE_PATH) to point it at
# /.netlify/functions/* directly, which does work. `ringdrill download
# <slug>` still uses /d/<slug> and will return 404 against this mode.
#
# AUTH_MODE=mock (ADR-0073) so sign-in works with no mail provider and no
# signing key: /api/auth/start-email returns the code in the response body and
# the sign-in screen fills it in. The default is `live`, which needs both, so
# without this local sign-in fails in a way that looks like a bug rather than a
# missing setting. Override with LOCAL_AUTH_MODE=live to exercise the real
# adapters against locally configured providers.
#
# Safe here and only here: lib/auth/mock.js refuses to load when
# CONTEXT=production, so this cannot follow a build into a production deploy.
# MAIL_PROVIDER=console (ADR-0075) is a separate axis and is needed too. It
# defaults to `resend`, which refuses to boot without RESEND_API_KEY — so
# without this, start-email and every invitation answer 500 even under
# AUTH_MODE=mock. The console adapter prints the whole message, code and magic
# link included, to this terminal.
# Generate the Ed25519 pair that signs access tokens (ADR-0025). Writes to
# .secrets/, which is gitignored, rather than stdout: a private key echoed into
# a terminal lands in scrollback and shell history, and selecting it with a
# mouse is where truncated copies come from.
#
# Only needed for a deployment running AUTH_MODE=live. `make netlify-dev` uses
# mock, which mints unsigned test tokens and needs no key material at all.
auth-keys:
node tools/generate-signing-keys.mjs
LOCAL_AUTH_MODE ?= mock
LOCAL_MAIL_PROVIDER ?= console
# Every emailed link is built from this and there is no fallback, so the
# functions refuse to send without it (see appOrigin in lib/shared.js). The apex
# is the right value even locally: the links are not clickable from here either
# way, and using the real one keeps the printed mail readable.
LOCAL_APP_ORIGIN ?= https://ringdrill.app
LOCAL_PWA_ORIGIN ?= https://web.ringdrill.app
netlify-dev:
npm install
ADMIN_TOKEN=$(LOCAL_ADMIN_TOKEN) AUTH_MODE=$(LOCAL_AUTH_MODE) MAIL_PROVIDER=$(LOCAL_MAIL_PROVIDER) PUBLIC_APP_ORIGIN=$(LOCAL_APP_ORIGIN) PUBLIC_PWA_ORIGIN=$(LOCAL_PWA_ORIGIN) npx netlify functions:serve --port 8888
# Local Astro dev server for the site/ project. Runs `astro dev` with HMR
# at http://localhost:4321/. Most pages need no backend; the CTAs link to
# the live web.ringdrill.app and play.google.com so there is nothing to stub.
#
# The on-demand /catalog route is the exception: it fetches
# PUBLIC_RINGDRILL_API_BASE + /api/market-feed at request time. Exporting
# $(LOCAL_BASE_URL) here means a `make netlify-dev` + `make catalog-seed` in
# another shell is enough to see seeded local plans at /catalog — the same
# two-shell workflow ADR-0013 already documents for the app and CLI. Astro
# falls back to the production API origin when this is unset, so a bare
# `make site-dev` still works standalone.
site-dev:
npm --prefix site install
PUBLIC_RINGDRILL_API_BASE=$(LOCAL_BASE_URL) npm --prefix site run dev
catalog-seed:
@test -f $(SEED_DRILL) || { echo "Seed file $(SEED_DRILL) not found. Set SEED_DRILL=<path>"; exit 1; }
RINGDRILL_BASE_URL=$(LOCAL_BASE_URL) \
RINGDRILL_ADMIN_TOKEN=$(LOCAL_ADMIN_TOKEN) \
RINGDRILL_FUNCTIONS_BASE_PATH=$(LOCAL_FUNCTIONS_BASE_PATH) \
dart run bin/ringdrill.dart upload $(SEED_DRILL) --published
# Seed the local catalog with the two store-screenshot demo plans (slugs
# `demo-no` and `demo-en`), so they can be opened straight from the in-app
# catalog instead of importing a file by hand. Requires `make netlify-dev`
# running in another shell, and the app started with
# `--dart-define=RINGDRILL_LOCAL_BASE_URL=$(LOCAL_BASE_URL)` so it talks to
# the local backend. Regenerate the files first with
# `python3 tools/screenshots/make_demo_drills.py` if they are missing.
DEMO_DRILLS := tools/screenshots/demo-no.drill tools/screenshots/demo-en.drill
catalog-seed-demos:
@for f in $(DEMO_DRILLS); do \
test -f $$f || { echo "Missing $$f. Run: python3 tools/screenshots/make_demo_drills.py"; exit 1; }; \
done
@for f in $(DEMO_DRILLS); do \
echo "Uploading $$f ..."; \
RINGDRILL_BASE_URL=$(LOCAL_BASE_URL) \
RINGDRILL_ADMIN_TOKEN=$(LOCAL_ADMIN_TOKEN) \
RINGDRILL_FUNCTIONS_BASE_PATH=$(LOCAL_FUNCTIONS_BASE_PATH) \
dart run bin/ringdrill.dart upload $$f --published || exit 1; \
done
# Convenience one-shot for local /catalog work: starts `make netlify-dev` in
# the background, waits for it to come up, seeds it with both `catalog-seed`
# (test-7x.drill, no language) and `catalog-seed-demos` (demo-no/demo-en,
# nb/en), then attaches to the backend so Ctrl-C stops it cleanly — no more
# juggling two shells just to get a populated local catalog. Run `make
# site-dev` in a second shell afterwards to browse it at /catalog.
netlify-dev-catalog-seed-all:
@echo "Starting local backend ($(LOCAL_BASE_URL)) in the background..."
@$(MAKE) netlify-dev & \
SERVER_PID=$$!; \
trap 'echo ""; echo "Stopping local backend (pid $$SERVER_PID)..."; kill $$SERVER_PID 2>/dev/null; pkill -P $$SERVER_PID 2>/dev/null; wait $$SERVER_PID 2>/dev/null' EXIT INT TERM; \
echo "Waiting for $(LOCAL_BASE_URL) to come up..."; \
until curl -sf "$(LOCAL_BASE_URL)/.netlify/functions/market-feed" >/dev/null 2>&1; do \
if ! kill -0 $$SERVER_PID 2>/dev/null; then \
echo "ERROR: local backend exited before it came up. See the output above."; \
exit 1; \
fi; \
sleep 1; \
done; \
echo "Backend is up. Seeding catalog..."; \
$(MAKE) catalog-seed; \
$(MAKE) catalog-seed-demos; \
echo ""; \
echo "Local backend running at $(LOCAL_BASE_URL). Press Ctrl-C to stop."; \
wait $$SERVER_PID
catalog-feed:
RINGDRILL_BASE_URL=$(LOCAL_BASE_URL) \
RINGDRILL_FUNCTIONS_BASE_PATH=$(LOCAL_FUNCTIONS_BASE_PATH) \
dart run bin/ringdrill.dart feed
catalog-reset:
rm -rf .netlify/blobs-serve
@echo "Local blob store cleared. Restart 'make netlify-dev'."
# Bump pubspec version, prepend a changelog entry, commit and tag.
# Usage:
# make release-tag VERSION=1.0.3+17 # explicit version
# make release-tag # auto-bump build number only
#
# VERSION must follow Flutter's `X.Y.Z+N` shape (semver + build number),
# matching the format already used for the existing `1.0.0+2` tag and for
# the `version:` line in pubspec.yaml.
#
# Auto-bump mode: when VERSION is not given, the current `X.Y.Z+N` in
# pubspec.yaml is read and `N` is incremented by 1. X.Y.Z stays put. Use
# this for shorebird patches and other release cuts where the user-facing
# semver does not change. To move semver (new minor, new major, etc.),
# pass VERSION explicitly.
#
# The changelog window is `git log <last-tag>..HEAD`. We use the most
# recent annotated/lightweight tag rather than scanning pubspec history,
# so each release-tag invocation lines up cleanly with the previous one
# even if someone hand-edited pubspec.yaml in between. `--no-merges`
# keeps the entry to actual feature/fix commits.
#
# The annotated tag (`git tag -a`) means `git describe` keeps working and
# GitHub renders a Release page out of the box. Push afterwards with:
# git push --follow-tags
#
# Guard rails:
# - require-clean-tree first, so the version bump commit is the only
# thing on top of the previous release;
# - VERSION (if supplied) must be shaped correctly;
# - build number `+N` MUST be strictly greater than the current pubspec
# build number, independent of X.Y.Z. App Store and Play Store both
# require monotonically increasing build numbers across uploads, so
# `1.0.3+25 -> 1.1.0+1` is rejected even though semver moved forward;
# - refuses to overwrite an existing tag;
# - refuses to "bump" to the version pubspec.yaml is already on (would
# produce an empty commit and a misleading tag).
release-tag: require-clean-tree
@set -e; \
CURRENT=$$(awk '/^version:/ {print $$2; exit}' pubspec.yaml); \
CUR_BASE=$$(echo "$$CURRENT" | cut -d'+' -f1); \
CUR_BUILD=$$(echo "$$CURRENT" | cut -d'+' -f2); \
case "$$CUR_BUILD" in ''|*[!0-9]*) \
echo "ERROR: cannot parse build number from pubspec version '$$CURRENT'"; \
exit 1;; \
esac; \
if [ -n "$(VERSION)" ]; then \
NEW_VERSION="$(VERSION)"; \
else \
BUMPED="$$CUR_BASE+$$((CUR_BUILD + 1))"; \
if [ ! -t 0 ]; then \
echo "No VERSION given (non-interactive). Auto-bumping: $$CURRENT -> $$BUMPED"; \
NEW_VERSION="$$BUMPED"; \
else \
echo ""; \
echo "Current version: $$CURRENT"; \
echo ""; \
echo " [1] Increment build → $$BUMPED (Enter)"; \
echo " [2] Enter version"; \
echo " [3] Cancel"; \
echo ""; \
printf "Select [1]: "; \
read CHOICE; \
case "$$CHOICE" in \
""|1) NEW_VERSION="$$BUMPED";; \
2) \
while true; do \
printf "New version (X.Y.Z+N, empty to cancel): "; \
read NEW_VERSION; \
if [ -z "$$NEW_VERSION" ]; then \
echo "Cancelled."; exit 1; \
fi; \
if ! echo "$$NEW_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\+[0-9]+$$'; then \
echo "Invalid. Expected X.Y.Z+N (e.g. 1.2.3+45)."; \
continue; \
fi; \
IN_BUILD=$$(echo "$$NEW_VERSION" | cut -d'+' -f2); \
if [ "$$IN_BUILD" -le "$$CUR_BUILD" ]; then \
echo "Invalid. Build number must be > $$CUR_BUILD (got $$IN_BUILD). Store build numbers must increase monotonically."; \
continue; \
fi; \
break; \
done;; \
3) echo "Cancelled."; exit 1;; \
*) echo "Invalid choice."; exit 1;; \
esac; \
fi; \
fi; \
echo "$$NEW_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\+[0-9]+$$' || { \
echo "ERROR: VERSION must look like 1.2.3+45, got '$$NEW_VERSION'"; \
exit 1; \
}; \
NEW_BUILD=$$(echo "$$NEW_VERSION" | cut -d'+' -f2); \
if [ "$$NEW_BUILD" -le "$$CUR_BUILD" ]; then \
echo "ERROR: build number must be strictly greater than $$CUR_BUILD (got $$NEW_BUILD)."; \
echo " Store build numbers must increase monotonically, independent of X.Y.Z."; \
exit 1; \
fi; \
if git rev-parse --verify --quiet "refs/tags/$$NEW_VERSION" >/dev/null; then \
echo "ERROR: tag $$NEW_VERSION already exists"; \
exit 1; \
fi; \
if [ "$$CURRENT" = "$$NEW_VERSION" ]; then \
echo "ERROR: pubspec.yaml is already at $$NEW_VERSION; pick a higher version"; \
exit 1; \
fi; \
echo "Bumping pubspec.yaml: $$CURRENT -> $$NEW_VERSION"; \
sed -i.bak -E "s/^version: .+/version: $$NEW_VERSION/" pubspec.yaml && rm pubspec.yaml.bak; \
PREV_TAG=$$(git describe --tags --abbrev=0 2>/dev/null || true); \
if [ -n "$$PREV_TAG" ]; then RANGE="$$PREV_TAG..HEAD"; else RANGE="HEAD"; fi; \
DATE=$$(date +%F); \
{ \
echo "## $$NEW_VERSION - $$DATE"; \
echo ""; \
if [ -n "$$PREV_TAG" ]; then echo "Changes since $$PREV_TAG:"; \
else echo "Initial changelog entry."; fi; \
echo ""; \
git log --no-merges --pretty=format:'- %s (%h)' $$RANGE; \
echo ""; \
echo ""; \
if [ -f CHANGELOG.md ]; then cat CHANGELOG.md; fi; \
} > CHANGELOG.md.new && mv CHANGELOG.md.new CHANGELOG.md; \
git add pubspec.yaml CHANGELOG.md; \
git commit -m "Released $$NEW_VERSION"; \
git tag -a "$$NEW_VERSION" -m "Released $$NEW_VERSION"; \
echo ""; \
echo "Created tag $$NEW_VERSION. Push with:"; \
echo " git push --follow-tags"
# One-shot release: bump version + tag, then build web + Android + iOS.
# Usage:
# make release VERSION=1.0.3+17 # explicit version
# make release # auto-bump build number only
#
# Order matters:
# 1. release-tag bumps pubspec.yaml, prepends CHANGELOG.md, commits and
# creates the annotated tag. The version label baked into every build
# below is read from pubspec.yaml, so the bump MUST happen first.
# When VERSION is omitted, release-tag increments the build number
# (`X.Y.Z+N` -> `X.Y.Z+N+1`) from pubspec.yaml.
# 2. release-web, release-android and release-ios run sequentially. They
# share build/ output and share dart_define inputs, so parallelism
# would step on itself. iOS also needs a macOS host with Xcode.
#
# Does NOT push. The tag is local until you run:
# git push --follow-tags
# Intentional — gives one last look at tag, CHANGELOG and built artifacts
# before publishing.
#
# If a build step fails midway, undo the local tag and commit with:
# git tag -d <version> && git reset --hard HEAD~1
# then re-run after fixing the cause. The current tag is the one at HEAD
# (`git tag --points-at HEAD`).
release: release-tag release-web release-android release-ios
@VERSION_OUT=$$(awk '/^version:/ {print $$2; exit}' pubspec.yaml); \
echo ""; \
echo "Release $$VERSION_OUT built (web + android + ios). Publish with:"; \
echo " make publish"
# One-shot Shorebird patch for both stores. Code-push to the X.Y.Z+N
# already on Shorebird's CDN — does NOT bump pubspec.yaml and does NOT
# create a git tag. The git commit metadata baked in via
# $(DART_DEFINE_GIT) still makes the patched binary traceable on the
# About page and in Sentry.
#
# Order matters and the steps are sequential:
# 1. patch-android (works on any host) builds the AAB delta, uploads
# to Shorebird, then runs sentry_dart_plugin to push the new
# obfuscation mapping. Mapping is tied to the patch-specific
# $(GIT_COMMIT), so Sentry can still resolve obfuscated stack
# traces from this patch.
# 2. patch-ios runs the same flow against the iOS released version.
# Requires macOS + Xcode; on Linux/Windows this target will fail.
#
# Shorebird does NOT have a single command that patches both platforms
# in one operation — `shorebird patch` takes a single platform
# argument, and the iOS half can only run on macOS. Two operations is
# the only path; this target just chains them so the user does not
# have to.
#
# If the Android half succeeds but the iOS half fails, the Android
# patch is already live. Re-running `make patch` will then attempt a
# second Android patch on top of the first, which Shorebird will
# refuse unless --allow-native-diffs is set. The right recovery is to
# fix the iOS issue and run `make patch-ios` alone.
patch: patch-android patch-ios
@VERSION_OUT=$$(awk '/^version:/ {print $$2; exit}' pubspec.yaml); \
echo ""; \
echo "Patched $$VERSION_OUT (android + ios) via Shorebird code-push."; \
echo "No tag was created; pubspec.yaml is unchanged."
# Generate Google Play release-notes scaffolding for the current pubspec
# version. Writes store/release-notes/google-play/<version>.txt with the
# two-locale wrapper that Play's Console import expects:
#
# <en-US>
# ...
# </en-US>
# <no-NO>
# ...
# </no-NO>
#
# Behaviour:
# - Version comes from pubspec.yaml unless VERSION= is passed.
# - English block is pre-filled from the CHANGELOG.md entry for that
# version, with `docs/test/chore/build/refactor` commits filtered out
# and the trailing `(abcdef0)` SHA stripped. The result is raw material
# to distill into store-friendly copy, not the final text.
# - Norwegian block stays as a placeholder for now (translation deferred).
# - Refuses to overwrite an existing file unless FORCE=1.
# - Reports the per-locale character count and warns if either block
# exceeds Play's 500-character per-release-note limit.
#
# Usage:
# make release-notes # current pubspec version
# make release-notes VERSION=1.0.3+27 # explicit version
# make release-notes FORCE=1 # overwrite an existing file
#
# Not wired into `make release` on purpose: notes are written by hand
# AFTER the bump, often during the upload step in Play Console, so the
# author can read the final CHANGELOG entry before distilling.
release-notes:
@set -e; \
if [ -n "$(VERSION)" ]; then \
VER="$(VERSION)"; \
else \
VER=$$(awk '/^version:/ {print $$2; exit}' pubspec.yaml); \
fi; \
if [ -z "$$VER" ]; then \
echo "ERROR: could not resolve version. Pass VERSION= or set version in pubspec.yaml."; \
exit 1; \
fi; \
DIR=store/release-notes/google-play; \
OUT="$$DIR/$$VER.txt"; \
mkdir -p "$$DIR"; \
if [ -f "$$OUT" ] && [ "$(FORCE)" != "1" ]; then \
echo "ERROR: $$OUT already exists. Re-run with FORCE=1 to overwrite."; \
exit 1; \
fi; \
HINT=$$(awk -v ver="$$VER" ' \
index($$0, "## " ver " ") == 1 {found=1; next} \
found && /^## / {exit} \
found {print} \
' CHANGELOG.md \
| grep -E '^- ' \
| grep -vE '^- (docs|test|chore|build|refactor)(\(|:)' \
| sed -E 's/ \([0-9a-f]{7,}\)$$//'); \
{ \
echo "<en-US>"; \
if [ -n "$$HINT" ]; then \
printf '%s\n' "$$HINT"; \
else \
echo "Write English release notes here (max 500 chars)."; \
fi; \
echo "</en-US>"; \
echo "<no-NO>"; \
echo "Skriv norske versjonsnotater her (maks 500 tegn)."; \
echo "</no-NO>"; \
} > "$$OUT"; \
echo "Wrote $$OUT"; \
echo ""; \
for LOC in en-US no-NO; do \
BODY=$$(awk -v loc="$$LOC" '$$0 == "<" loc ">" {f=1; next} $$0 == "</" loc ">" {f=0} f' "$$OUT"); \
LEN=$$(printf '%s' "$$BODY" | wc -c | tr -d ' '); \
if [ "$$LEN" -gt 500 ]; then \
echo "WARN: $$LOC is $$LEN chars, Play caps each note at 500."; \
else \
echo "OK: $$LOC is $$LEN chars (limit 500)."; \
fi; \
done
# Push the release commit and tag to origin. Pair with `make release`.
#
# What ends up where:
# - Web: `git push` triggers .github/workflows/deploy-web.yml, which
# rebuilds on GHA, uploads symbols to Sentry, and deploys to Netlify.
# ~5-7 minutes from push to live on ringdrill.app.
# - Android/iOS: already on Shorebird's CDN — `shorebird release` uploaded
# during `make release`. The push only ships the commit/tag on GitHub
# for traceability and About-page deep links.
#
# Guards:
# - require-clean-tree so no untracked changes ride along with the push;
# - HEAD must carry a tag, otherwise there is no release to publish and
# `--follow-tags` would silently just push commits.
publish: require-clean-tree
@TAG=$$(git tag --points-at HEAD | head -n1); \
if [ -z "$$TAG" ]; then \
echo "ERROR: HEAD has no tag. Run 'make release VERSION=...' first."; \
exit 1; \
fi; \
echo "Publishing $$TAG ..."; \
git push --follow-tags