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
143 changes: 42 additions & 101 deletions .github/workflows/dart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,122 +2,63 @@ name: Dart

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
FLUTTER_VERSION: "3.35.0"
FLUTTER_VERSION: "3.47.5"

jobs:
discover:
quality:
runs-on: ubuntu-latest
outputs:
packages: ${{ steps.packages.outputs.packages }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Install melos
run: dart pub global activate melos
- name: Bootstrap packages
run: melos bootstrap
- name: Get packages
id: packages
run: |
# Determine the base ref for comparison
if [ "${{ github.event_name }}" == "pull_request" ]; then
BASE_REF="origin/${{ github.base_ref }}"
else
BASE_REF="HEAD~1"
fi


# Get packages that have changes using melos diff
packages=$(melos list --diff="$BASE_REF" --json | jq -c "[.[] | select(.name != \"flutter_forge_workspace\") | .name]" 2>/dev/null || echo '[]')
if [ "$packages" = "" ] || [ "$packages" = "null" ]; then
packages="[]"
fi
echo "packages=$packages" >> $GITHUB_OUTPUT
echo "Changed packages: $packages"

build:
needs: discover
runs-on: ubuntu-latest
if: needs.discover.outputs.packages != '[]'
strategy:
matrix:
package: ${{ fromJson(needs.discover.outputs.packages) }}
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
- uses: actions/setup-python@v5
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true

- name: Install dependencies
run: |
dart pub global activate melos
melos bootstrap --scope="${{ matrix.package }}"

- name: Format package
# Brick templates are mustache, not parseable Dart, and `dart format`
# has no exclude of its own - so filter the paths before it sees them.
run: |
melos exec --scope="${{ matrix.package }}" -- \
"find . -name '*.dart' -not -path '*/__brick__/*' -not -path '*/.dart_tool/*' \
-exec dart format --set-exit-if-changed {} +"

- name: Analyze package
run: melos exec --scope="${{ matrix.package }}" -- dart analyze --fatal-infos
python-version: "3.x"
- name: Resolve workspace dependencies
run: flutter pub get --enforce-lockfile
- name: Verify coverage gate behavior
run: python3 -m unittest discover -s tools/tests
- name: Check formatting and analyze workspace
run: dart run melos run lint
- name: Require 100% coverage in every package
run: python3 tools/coverage.py
- name: Save test logs and coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
name: workspace-coverage
path: |
coverage/
packages/**/coverage/
plugins/**/coverage/
tools/**/coverage/

test:
needs: [ discover, build ]
runs-on: ubuntu-latest
if: needs.discover.outputs.packages != '[]'
strategy:
matrix:
package: ${{ fromJson(needs.discover.outputs.packages) }}
native-permissions:
runs-on: macos-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- uses: subosito/flutter-action@v2
- uses: actions/setup-java@v4
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true

- name: Install dependencies
run: |
dart pub global activate melos
melos bootstrap --scope="${{ matrix.package }}"

- name: Run tests
id: test-step
run: |
# Try Flutter test first (will only run if package has test dir and is Flutter package)
if melos exec --scope="${{ matrix.package }}" --dir-exists=test --flutter -- flutter test --coverage 2>/dev/null; then
echo "has_tests=true" >> $GITHUB_OUTPUT
# Try Dart test (will only run if package has test dir and is not Flutter package)
elif melos exec --scope="${{ matrix.package }}" --dir-exists=test --no-flutter -- dart test --coverage 2>/dev/null; then
echo "has_tests=true" >> $GITHUB_OUTPUT
else
echo "No tests to run for ${{ matrix.package }}"
echo "has_tests=false" >> $GITHUB_OUTPUT
fi

# Get package path for coverage upload
package_path=$(melos list --scope="${{ matrix.package }}" --json | jq -r ".[0].path")
echo "package_path=$package_path" >> $GITHUB_OUTPUT

- name: Upload coverage reports to Codecov
if: steps.test-step.outputs.has_tests == 'true'
uses: codecov/codecov-action@v5
distribution: temurin
java-version: "21"
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Require 100% native permission-plugin coverage
run: python3 plugins/contact_permission/native_tests/run.py
- name: Save native coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: ${{ matrix.package }}
files: ${{ steps.test-step.outputs.package_path }}/coverage/lcov.info
name: native-permission-coverage
path: |
plugins/contact_permission/native_tests/coverage/
plugins/contact_permission/native_tests/android/build/reports/
12 changes: 6 additions & 6 deletions .github/workflows/nonstop-template.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: NonStop generated template
on:
pull_request:
paths: ['packages/nonstop_cli/**', '.github/workflows/nonstop-template.yml']
paths: ['packages/nonstop_cli/**', 'packages/cli_core/**', 'pubspec.yaml', 'pubspec.lock', '.github/workflows/nonstop-template.yml']
push:
branches: [main]
paths: ['packages/nonstop_cli/**', '.github/workflows/nonstop-template.yml']
paths: ['packages/nonstop_cli/**', 'packages/cli_core/**', 'pubspec.yaml', 'pubspec.lock', '.github/workflows/nonstop-template.yml']
permissions:
contents: read
jobs:
Expand All @@ -17,9 +17,9 @@ jobs:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
flutter-version: '3.47.5'
cache: true
- run: dart pub get
- run: flutter pub get --enforce-lockfile
- run: dart analyze --fatal-infos lib test tool
- run: dart test --exclude-tags version-verify
generated:
Expand All @@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.44.0'
flutter-version: '3.47.5'
cache: true
- run: dart pub get
- run: flutter pub get --enforce-lockfile
- run: dart run tool/verify_template.dart ${{ matrix.profile }}
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ full_coverage_test.dart
**/example/linux/
**/example/macos/
**/example/windows/

# Keep a reproducible resolution for the native Dart workspace.
!/pubspec.lock

# Python coverage-gate test caches.
__pycache__/
*.pyc
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ optional-module configurations.

---

## Development and tests

Use Flutter 3.47.5 and run `flutter pub get --enforce-lockfile` from the repository root. Melos configuration is in `pubspec.yaml`.

Run `python3 tools/coverage.py` (or `dart run melos run test`) to test every workspace package and require 100% executable Dart line coverage. See [testing and dependency maintenance](docs/testing.md) for focused runs, generated-template validation, and coverage scope.

## 🔗 Connect with NonStop

<div align="center">
Expand Down
6 changes: 3 additions & 3 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ include: package:flutter_lints/flutter.yaml

analyzer:
exclude:
- lib/generated/**
- test/**
- packages/nonstop_cli/bricks/**
- packages/nonstop_cli/bricks/**/__brick__/**
- "**/coverage/**"
- "**/build/**"
linter:
rules:
constant_identifier_names: false
Expand Down
76 changes: 76 additions & 0 deletions docs/coverage-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Coverage and dependency verification

Verified on 2026-09-21 after updating from `origin/main` at `2d1b5ac`, using Flutter 3.47.5 and Dart 3.13.4.

The final fresh `python3 tools/coverage.py` run exited successfully: **24/24 workspace packages passed, with 11,116/11,116 executable Dart lines covered (100%)**. This includes libraries, examples, command entrypoints, Mason hooks, repository tools, and the generated-template verification script.

## Workspace results

| Package | Covered / executable lines | Coverage |
| --- | ---: | ---: |
| `flutter_forge` | 92 / 92 | 100% |
| `cli_core` | 85 / 85 | 100% |
| `connectivity_wrapper_example` | 129 / 129 | 100% |
| `connectivity_wrapper` | 160 / 160 | 100% |
| `dzod_example` | 2,808 / 2,808 | 100% |
| `dzod` | 4,507 / 4,507 | 100% |
| `example_html_rich_text` | 46 / 46 | 100% |
| `html_rich_text` | 73 / 73 | 100% |
| `morse_tap_example` | 367 / 367 | 100% |
| `morse_tap` | 415 / 415 | 100% |
| `flutter_app_for_mono_repo_hooks` | 17 / 17 | 100% |
| `flutter_package_for_mono_repo_hooks` | 16 / 16 | 100% |
| `flutter_plugin_for_mono_repo_hooks` | 17 / 17 | 100% |
| `flutter_project_with_mono_repo_hooks` | 103 / 103 | 100% |
| `nonstop_cli` | 759 / 759 | 100% |
| `ns_firebase_utils` | 246 / 246 | 100% |
| `ns_intl_phone_input_example` | 53 / 53 | 100% |
| `ns_intl_phone_input` | 282 / 282 | 100% |
| `ns_utils` | 583 / 583 | 100% |
| `timer_button_example` | 38 / 38 | 100% |
| `timer_button` | 79 / 79 | 100% |
| `contact_permission_example` | 30 / 30 | 100% |
| `contact_permission` | 16 / 16 | 100% |
| `readme_sync` | 195 / 195 | 100% |
| **Total** | **11,116 / 11,116** | **100%** |

## Generated templates and native sources

Mustache templates were rendered before validation. All 512 module selections passed the template contract checks. Six representative generated workspaces passed analysis, tests, 100% handwritten Dart line coverage, and web builds:

| Profile | Covered / executable lines | Web build |
| --- | ---: | --- |
| Full | 2,396 / 2,396 | Passed |
| Default | 2,393 / 2,393 | Passed |
| Minimal | 766 / 766 | Passed |
| Authentication | 950 / 950 | Passed |
| Firestore | 781 / 781 | Passed |
| Network | 1,120 / 1,120 | Passed |

The full profile's existing temporary application received standard Flutter web platform scaffolding before its build; its application code, dependencies, and tests were preserved.

The contact-permission plugin's production native sources also passed their unit coverage gates: **Android Kotlin 58/58**, **Apple Swift/Objective-C 44/44**. A disposable Flutter Android host built a debug APK with the upgraded plugin in both supported Kotlin modes. Native unit tests use SDK boundary doubles; actual OS permission dialogs and a real iOS embedding build were not tested.

## Dependency upgrades and remaining constraints

Dart/Flutter dependencies, generated-template dependencies, Node development tooling, and the Android plugin/tooling were upgraded. The workspace now uses native Dart pub workspaces and Melos 8, with a shared checked-in Dart lockfile. The pnpm lockfile is updated, and its audit reported zero vulnerabilities.

`flutter pub outdated --json` reported **no resolvable upgrades**. Eleven direct/transitive packages have newer releases blocked by the current dependency graph. The key constraints are Flutter's `test_api` pin (holding analyzer/test and related tooling), `fake_cloud_firestore`'s Equatable 2 requirement, and AndroidX Core 1.19's compile SDK 37 requirement (this plugin uses SDK 36). These dependencies remain at their latest compatible versions.

## Reproduce and inspect

```sh
flutter pub get --enforce-lockfile
dart run melos run lint
python3 -m unittest discover -s tools/tests
python3 tools/coverage.py
python3 plugins/contact_permission/native_tests/run.py
# From packages/nonstop_cli, repeat for each profile above:
dart run tool/verify_template.dart full
```

The coverage gate's 12 regression tests passed. Formatting and strict analysis passed. The CLI's separate generated-version build check remains intentionally tagged `version-verify`; the unit coverage result includes all executable CLI/tool lines and the separate template contract/build checks above passed.

Fresh workspace details are in `coverage/summary.json`; each package retains its LCOV report and test log under `coverage/`. Native details are in `plugins/contact_permission/native_tests/coverage/summary.json`. Reports and build outputs are ignored by Git and regenerated by CI. The recorded percentages are executable **line** coverage, not branch coverage.

See [testing and dependency maintenance](testing.md) for gate behavior, SDK requirements, compatibility changes, and focused commands.
58 changes: 58 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Tests and dependency maintenance

The [verified coverage report](coverage-report.md) records the package totals, generated-template builds, native coverage, and dependency compatibility limits.

Use Flutter 3.47.5 (Dart 3.13.4), Python 3.10 or newer, and Node.js 24 or newer. The repository uses a native Dart pub workspace and Melos 8; configuration lives in the root `pubspec.yaml`.

```sh
flutter pub get --enforce-lockfile
dart run melos run lint
python3 -m unittest discover -s tools/tests
python3 tools/coverage.py
# Equivalent full coverage command:
dart run melos run test
```

The coverage command discovers workspace packages rather than selecting only folders that already have tests. It includes libraries, executable entrypoints, example applications, Mason hook packages, README synchronization, repository version/bundle tools, and the generated-template verification script. Every package must have real unit or widget tests and a nonempty report. The gate requires every executable Dart line to be hit; it compares integer counts rather than a rounded percentage. Compile-time constants and export-only libraries do not contain executable lines.

Production libraries are loaded alongside an existing test suite so a never-imported library cannot silently disappear. Missing source records fail unless the file contains only recognized declarations without executable code. Temporary coverage entrypoints are restored or removed after the run. Each Flutter run clears its generated test kernel cache to avoid stale source metadata. Flutter coverage is collected without VM package filters, then restricted to the package’s owned files; this avoids a Dart 3.13.4 source-report crash and includes entrypoints outside `lib/`. The generated runner lives under `coverage/` and uses the installed Flutter tool without modifying the SDK. Raw logs and LCOV reports are written to each package's `coverage/` directory; the complete result is `coverage/summary.json`. These generated reports are ignored by Git.

Run a focused package while developing:

```sh
python3 tools/coverage.py --package ns_utils
python3 tools/coverage.py --package packages/timer_button/example
```

`--report-only` inspects existing reports and writes `summary-existing.json`; it does not establish a fresh passing result.

Mustache brick source is not runnable Dart until rendered. The separate generated-template workflow renders representative module combinations, analyzes them, and requires 100% handwritten Dart coverage inside the generated workspace. The template contract suite additionally validates every module selection. Hook bundles exclude local tests/build artifacts and workspace-only `resolution` metadata.

## Dependencies

The shared root `pubspec.lock` and `pnpm-lock.yaml` are checked in. CI enforces the Dart lockfile so dependency resolution cannot drift between package jobs. When intentionally upgrading dependencies, run:

```sh
flutter pub upgrade --major-versions
flutter pub outdated
pnpm install --frozen-lockfile
pnpm audit
```

After an upgrade, rerun the full coverage gate, strict analysis, and generated-template checks. Some newest releases cannot coexist with Flutter SDK pins or upstream package constraints: the current graph uses Equatable 2.1 (required by fake_cloud_firestore) and analyzer 13.3/test 1.31.1 (compatible with Flutter's test_api pin). Do not force incompatible versions with dependency overrides.

The development-only `tmp` override selects a patched release because all-contributors-cli's transitive external-editor dependency requests an obsolete version. Its temporary-file creation and cleanup API is smoke-tested during the upgrade.

The Melos upgrade moves workspace configuration from `melos.yaml` into the root `pubspec.yaml`; run bootstrap and scripts from the repository root. DZod string refinements now correctly return `Schema<String>` instead of throwing an invalid cast. Place string-specific checks before those refinements, for example `z.string().min(3).startsWith('abc')`.

## Native permission-plugin tests

Run the Android Kotlin and Apple Swift/Objective-C unit suites on macOS with Xcode command-line tools and Java 21:

```sh
python3 plugins/contact_permission/native_tests/run.py
```

Use `--platform android` for the JVM suite alone or `--platform apple` for the Apple suite. The checked-in Gradle wrapper resolves the JVM test tooling. Both suites compile the production plugin files and require 100% measured native line coverage; reports are under `plugins/contact_permission/native_tests/coverage/` and the Android test build directory.

Platform test doubles make permission states, callbacks, registration, and lifecycle behavior deterministic. These unit tests do not open OS permission dialogs or establish device-level integration coverage. The separate `native-permissions` CI job runs both suites on macOS.
Loading
Loading