Skip to content
Open
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ jobs:
api/package-lock.json
worker/package-lock.json

- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- run: ./scripts/lint.sh

build:
Expand Down Expand Up @@ -62,6 +67,11 @@ jobs:
api/package-lock.json
worker/package-lock.json

- uses: subosito/flutter-action@v2
with:
channel: stable
cache: true

- run: ./scripts/test.sh

release-scripts:
Expand Down
1 change: 1 addition & 0 deletions .jules/forge.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
- **[2026-06-15]**: Removed obsolete Python/`uv` tooling from `AGENTS.md` as the project is now fully Node.js. Identified a gap where `worker` build checks were missing from `.github/workflows/ci.yml`. Added a `worker` job to `ci.yml` to run `npm ci` and `npm run build`, ensuring type safety and parity with `AGENTS.md` intent.
- **[2026-04-08]**: Updated `google-github-actions/auth` from v2 to v3 in `provision-worker.yml` to prevent deprecation issues. Extracted inline YAML scripts for Doppler secrets, Forge version resolution, worker notifications, and worker host config into reusable `scripts/` to enforce execution parity and Kickstart principles. Refined `AGENTS.md` to expose CI parity scripts (`./scripts/test.sh`, etc.) alongside targeted commands.
- **[2026-04-11]**: Updated `google-github-actions/auth` from v2 to v3 in `rollout-safety-net.yml` to prevent deprecation issues. Extracted inline YAML scripts for rollout safety net polling, setting image names, checking docker tags, and extracting GCP project IDs into reusable `scripts/` to enforce execution parity and Kickstart verification principles. Refined `scripts/run-analysis.js` to mitigate command injection risks by removing `shell: true` from `child_process.spawn`.
- **[2026-07-08]**: Discovered Dart SDK tool-version conflict causing CI parity failure (Flutter stable provides 3.11.0, pubspec required ^3.11.5). Calibrated `worker_flutter/pubspec.yaml` constraint to `>=3.11.0 <4.0.0` and integrated `subosito/flutter-action@v2` into `ci.yml` `lint` and `test` jobs to match local scripts. Updated `AGENTS.md` and verification scripts to include `flutter analyze` and `flutter test`.
7 changes: 6 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

## Tooling
- **Node.js**: 20+ (managed by `npm`)
- **Flutter**: Stable channel (managed by `flutter`)
- **Docker**: Required for simulation engine
- **Package Manager**: `npm` (Node)
- **Package Manager**: `npm` (Node), `pub` (Flutter)

## Setup
- **Install Node Dependencies**: `npm run install:all`
- **Install Flutter Dependencies**: `cd worker_flutter && flutter pub get`
- **Start Dev Server**: `npm run dev`

## Testing & Verification
Expand All @@ -25,6 +27,8 @@
- **API Unit Test**: `cd api && npm run test:unit`
- **API Ingestion Test**: `cd api && npm run test:ingestion`
- **Worker Unit Test**: `cd worker && npm run test:unit`
- **Worker Flutter Lint**: `cd worker_flutter && flutter analyze --no-fatal-infos`
- **Worker Flutter Test**: `cd worker_flutter && flutter test`
- **Worker Build**: `cd worker && npm run build`
- **Worker Install**: `cd worker && npm install`
- **Worker Dev**: `cd worker && npm run dev`
Expand All @@ -38,3 +42,4 @@
- `frontend/`: React app
- `api/`: Next.js API, ingestion, and analysis
- `worker/`: Node.js + Docker simulation runner
- `worker_flutter/`: Cross-platform desktop worker for Magic Bracket Simulator
4 changes: 4 additions & 0 deletions scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ cd frontend && npm ci && npm run lint && cd ..
# API
echo "=== API lint ==="
cd api && npm ci && npm run lint && cd ..

# Worker Flutter
echo "=== Worker Flutter lint ==="
cd worker_flutter && flutter pub get && flutter analyze --no-fatal-infos && cd ..
4 changes: 4 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ npm run test:ingestion && cd ..
# Worker tests
echo "=== Worker unit tests ==="
cd worker && npm ci && npm run test:unit && cd ..

# Worker Flutter tests
echo "=== Worker Flutter tests ==="
cd worker_flutter && flutter pub get && flutter test && cd ..
1 change: 0 additions & 1 deletion worker_flutter/lib/api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'dart:io';

import 'package:firebase_app_check/firebase_app_check.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:worker_flutter/main.dart';

Expand Down
2 changes: 1 addition & 1 deletion worker_flutter/lib/installer/install_progress_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class _InstallScreenState extends State<_InstallScreen> {
e,
st,
category: TelemetryCategory.installer,
extra: {'stage': _last.stage, 'message': _last.message},
contexts: {'stage': _last.stage, 'message': _last.message},
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions worker_flutter/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Future<void> _appMain() async {
e,
st,
category: TelemetryCategory.firebaseInit,
extra: {'projectId': fbOpts.projectId},
contexts: {'projectId': fbOpts.projectId},
);
}
}
Expand Down Expand Up @@ -404,7 +404,7 @@ Future<void> _activateAppCheck() async {
e,
st,
category: TelemetryCategory.boot,
extra: {'phase': 'appcheck_activate'},
contexts: {'phase': 'appcheck_activate'},
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion worker_flutter/lib/offline/db/app_db.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class AppDb extends _$AppDb {
/// test suite with `NativeDatabase.memory()` for hermetic, fast
/// integration tests that exercise the same SQL the app does at
/// runtime — but without touching disk.
AppDb.forTesting(QueryExecutor e) : super(e);
AppDb.forTesting(super.e);

@override
int get schemaVersion => 2;
Expand Down
6 changes: 3 additions & 3 deletions worker_flutter/lib/offline/deck_source.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:path/path.dart' as p;
/// One bundled-precon deck. The `.dck` content lives either in the
/// Flutter asset bundle (preferred — ships inside the app, available
/// instantly on first launch) or, for users with a separately-
/// installed Forge tree, in `<forgePath>/res/Decks/Commander/*.dck`.
/// installed Forge tree, in `&lt;forgePath&gt;/res/Decks/Commander/*.dck`.
class PreconDeck {
PreconDeck({
required this.displayName,
Expand Down Expand Up @@ -38,7 +38,7 @@ class PreconDeck {
}

/// Loads precons from the Flutter asset bundle PLUS any extra .dck
/// files the user may have dropped into `<forgePath>/res/Decks/
/// files the user may have dropped into `&lt;forgePath&gt;/res/Decks/
/// Commander/`. The bundled set is the floor — offline mode works
/// from a fresh install without a Forge download.
Future<List<PreconDeck>> loadBundledPrecons(String forgePath) async {
Expand Down Expand Up @@ -104,7 +104,7 @@ Future<List<PreconDeck>> loadBundledPrecons(String forgePath) async {
return out;
}

/// Write a bundled precon to `destDir/<filename>` and return the full
/// Write a bundled precon to `destDir/&lt;filename&gt;` and return the full
/// path Forge will read. No-op when the deck is already a real file.
Future<String> materializePrecon(PreconDeck deck, String destDir) async {
if (!deck.isBundled) return deck.path;
Expand Down
2 changes: 2 additions & 0 deletions worker_flutter/lib/sims/deck_picker_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ class DeckPickerSection extends StatefulWidget {
}

class _DeckPickerSectionState extends State<DeckPickerSection> {
// ignore: deprecated_member_use
late final ExpansionTileController _ctrl;

@override
void initState() {
super.initState();
// ignore: deprecated_member_use
_ctrl = ExpansionTileController();
}

Expand Down
36 changes: 20 additions & 16 deletions worker_flutter/lib/telemetry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class Telemetry {
StackTrace? stack, {
required TelemetryCategory category,
Map<String, String>? tags,
Map<String, dynamic>? extra,
Map<String, dynamic>? contexts,
}) async {
await Sentry.captureException(
error,
Expand All @@ -72,10 +72,8 @@ class Telemetry {
if (tags != null) {
tags.forEach(scope.setTag);
}
if (extra != null) {
// ignore: deprecated_member_use — `setExtra` is the documented
// 8.x API. Sentry's 9.x release moves to structured contexts.
extra.forEach(scope.setExtra);
if (contexts != null) {
contexts.forEach((key, value) => scope.setContexts(key, value));
}
},
);
Expand Down Expand Up @@ -111,18 +109,24 @@ SentryEvent? scrubPii(SentryEvent event, {Hint? hint}) {
? event
: event.copyWith(user: SentryUser(id: '[redacted]'));

// ignore: deprecated_member_use — see comment in Telemetry.captureError.
final extra = cleaned.extra;
if (extra != null) {
for (final key in extra.keys.toList()) {
if (_piiKeys.contains(key)) {
extra[key] = '[redacted]';
continue;
}
final value = extra[key];
if (value is String && _emailRegex.hasMatch(value)) {
extra[key] = value.replaceAll(_emailRegex, '[redacted-email]');
final contexts = cleaned.contexts;
for (final key in contexts.keys.toList()) {
if (_piiKeys.contains(key)) {
contexts[key] = '[redacted]';
continue;
}
final value = contexts[key];
if (value is Map<String, dynamic>) {
for (final nestedKey in value.keys.toList()) {
final nestedValue = value[nestedKey];
if (nestedValue is String && _emailRegex.hasMatch(nestedValue)) {
value[nestedKey] = nestedValue.replaceAll(_emailRegex, '[redacted-email]');
} else if (_piiKeys.contains(nestedKey)) {
value[nestedKey] = '[redacted]';
}
}
} else if (value is String && _emailRegex.hasMatch(value)) {
contexts[key] = value.replaceAll(_emailRegex, '[redacted-email]');
}
}

Expand Down
6 changes: 3 additions & 3 deletions worker_flutter/lib/worker/worker_engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class WorkerEngine {
e,
st,
category: TelemetryCategory.engineRuntime,
extra: {'phase': 'lease_writer_start'},
contexts: {'phase': 'lease_writer_start'},
);
}

Expand Down Expand Up @@ -167,7 +167,7 @@ class WorkerEngine {
e,
st,
category: TelemetryCategory.engineRuntime,
extra: {'phase': 'pending_listener_setup'},
contexts: {'phase': 'pending_listener_setup'},
);
}
}
Expand Down Expand Up @@ -305,7 +305,7 @@ class WorkerEngine {
e,
st,
category: TelemetryCategory.engineRuntime,
extra: {'phase': 'sim_run', 'jobId': sim.jobId, 'simId': sim.simId},
contexts: {'phase': 'sim_run', 'jobId': sim.jobId, 'simId': sim.simId},
);
await _claimer.reportTerminal(
sim: sim,
Expand Down
10 changes: 5 additions & 5 deletions worker_flutter/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -692,10 +692,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
version: "0.12.19"
version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
Expand Down Expand Up @@ -1193,10 +1193,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
version: "0.7.9"
tray_manager:
dependency: "direct main"
description:
Expand Down Expand Up @@ -1374,5 +1374,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.5 <4.0.0"
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.4"
2 changes: 1 addition & 1 deletion worker_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ publish_to: 'none'
version: 0.2.0+1

environment:
sdk: ^3.11.5
sdk: '>=3.11.0 <4.0.0'

dependencies:
flutter:
Expand Down
1 change: 0 additions & 1 deletion worker_flutter/test/offline/resume_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io';

import 'package:drift/drift.dart' show Value;
Expand Down
41 changes: 22 additions & 19 deletions worker_flutter/test/telemetry_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,42 @@ void main() {

group('scrubPii', () {
test('redacts email-shaped values in event extras', () {
final contexts = Contexts();
contexts['app_data'] = {
'note': 'user contacted me at someone@example.com about it',
'okField': 'no pii here',
};
final event = SentryEvent(message: const SentryMessage('hi')).copyWith(
extra: <String, dynamic>{
'note': 'user contacted me at someone@example.com about it',
'okField': 'no pii here',
},
contexts: contexts,
);

final scrubbed = scrubPii(event);

expect(scrubbed, isNotNull);
expect(scrubbed!.extra!['note'], contains('[redacted-email]'));
expect(scrubbed.extra!['note'], isNot(contains('someone@example.com')));
expect(scrubbed.extra!['okField'], 'no pii here');
final appData = scrubbed!.contexts['app_data'] as Map<String, dynamic>;
expect(appData['note'], contains('[redacted-email]'));
expect(appData['note'], isNot(contains('someone@example.com')));
expect(appData['okField'], 'no pii here');
});

test('redacts known PII keys', () {
final contexts = Contexts();
contexts['email'] = 'a@b.com';
contexts['uid'] = 'xyz';
contexts['displayName'] = 'Tyler';
contexts['user_id'] = '42';
contexts['safe'] = 'keep me';
final event = SentryEvent(message: const SentryMessage('hi')).copyWith(
extra: <String, dynamic>{
'email': 'a@b.com',
'uid': 'xyz',
'displayName': 'Tyler',
'user_id': '42',
'safe': 'keep me',
},
contexts: contexts,
);

final scrubbed = scrubPii(event);

expect(scrubbed!.extra!['email'], '[redacted]');
expect(scrubbed.extra!['uid'], '[redacted]');
expect(scrubbed.extra!['displayName'], '[redacted]');
expect(scrubbed.extra!['user_id'], '[redacted]');
expect(scrubbed.extra!['safe'], 'keep me');
expect(scrubbed!.contexts['email'], '[redacted]');
expect(scrubbed.contexts['uid'], '[redacted]');
expect(scrubbed.contexts['displayName'], '[redacted]');
expect(scrubbed.contexts['user_id'], '[redacted]');
expect(scrubbed.contexts['safe'], 'keep me');
});

test('replaces user with redacted placeholder', () {
Expand Down
1 change: 0 additions & 1 deletion worker_flutter/test/worker/log_uploader_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
Expand Down
Loading