diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fe5ad0e..84ffcda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -5,6 +5,7 @@ on: push: branches: [main] pull_request: + workflow_call: jobs: # The workspace contains a Flutter package (packages/insforge_flutter), so the @@ -13,12 +14,14 @@ jobs: analyze-and-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: subosito/flutter-action@v2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + - uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 with: channel: stable - name: Install dependencies run: flutter pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . - name: Analyze run: dart analyze . - name: Check version is in sync (both packages + version.dart) @@ -32,3 +35,9 @@ jobs: - name: Test insforge_flutter working-directory: packages/insforge_flutter run: flutter test + - name: Dry-run publish insforge + working-directory: packages/insforge + run: dart pub publish --dry-run + - name: Dry-run publish insforge_flutter + working-directory: packages/insforge_flutter + run: dart pub publish --dry-run diff --git a/.github/workflows/publish-insforge-flutter.yml b/.github/workflows/publish-insforge-flutter.yml new file mode 100644 index 0000000..ddc9f8b --- /dev/null +++ b/.github/workflows/publish-insforge-flutter.yml @@ -0,0 +1,92 @@ +name: Publish insforge_flutter to pub.dev + +on: + push: + tags: + - "insforge_flutter-v[0-9]+.[0-9]+.[0-9]+" + - "insforge_flutter-v[0-9]+.[0-9]+.[0-9]+-*" + - 'insforge_flutter-v[0-9]+.[0-9]+.[0-9]+\+*' + +jobs: + validate: + name: Validate release + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + stable: ${{ steps.version.outputs.stable }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 + + - name: Validate tag matches package version + id: version + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + version="$(sed -n 's/^version:[[:space:]]*//p' packages/insforge_flutter/pubspec.yaml)" + if [ "$RELEASE_TAG" != "insforge_flutter-v$version" ]; then + echo "Tag $RELEASE_TAG does not match insforge_flutter version $version." + exit 1 + fi + + echo "version=$version" >> "$GITHUB_OUTPUT" + if [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "stable=true" >> "$GITHUB_OUTPUT" + else + echo "stable=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check lockstep versions + # Run the dependency-free script directly so Dart does not try to + # resolve the root workspace, which also contains a Flutter package. + run: dart tool/set_version.dart --check + + - name: Verify matching insforge version is published + env: + RELEASE_VERSION: ${{ steps.version.outputs.version }} + run: | + if ! curl --fail --silent --show-error --retry 5 --retry-delay 10 \ + "https://pub.dev/api/packages/insforge/versions/$RELEASE_VERSION" \ + >/dev/null; then + echo "insforge $RELEASE_VERSION is not available on pub.dev yet." >&2 + echo "Publish insforge-v$RELEASE_VERSION first and wait for it to become available." >&2 + exit 1 + fi + + quality: + name: Run release quality checks + permissions: + contents: read + uses: ./.github/workflows/ci.yaml + + publish: + name: Publish insforge_flutter + needs: [validate, quality] + permissions: + id-token: write + uses: dart-lang/setup-dart/.github/workflows/publish.yml@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 + with: + working-directory: packages/insforge_flutter + + release: + name: Create stable GitHub Release + if: needs.validate.outputs.stable == 'true' + needs: [validate, publish] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + REPOSITORY: ${{ github.repository }} + run: | + if gh release view "$RELEASE_TAG" --repo "$REPOSITORY" >/dev/null 2>&1; then + echo "GitHub Release $RELEASE_TAG already exists" + else + gh release create "$RELEASE_TAG" --repo "$REPOSITORY" --generate-notes --verify-tag + fi diff --git a/.github/workflows/publish-insforge.yml b/.github/workflows/publish-insforge.yml new file mode 100644 index 0000000..9869bba --- /dev/null +++ b/.github/workflows/publish-insforge.yml @@ -0,0 +1,79 @@ +name: Publish insforge to pub.dev + +on: + push: + tags: + - "insforge-v[0-9]+.[0-9]+.[0-9]+" + - "insforge-v[0-9]+.[0-9]+.[0-9]+-*" + - 'insforge-v[0-9]+.[0-9]+.[0-9]+\+*' + +jobs: + validate: + name: Validate release + runs-on: ubuntu-latest + permissions: + contents: read + outputs: + stable: ${{ steps.version.outputs.stable }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 + + - name: Validate tag matches package version + id: version + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + version="$(sed -n 's/^version:[[:space:]]*//p' packages/insforge/pubspec.yaml)" + if [ "$RELEASE_TAG" != "insforge-v$version" ]; then + echo "Tag $RELEASE_TAG does not match insforge version $version." + exit 1 + fi + + if [[ "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "stable=true" >> "$GITHUB_OUTPUT" + else + echo "stable=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check lockstep versions + # Run the dependency-free script directly so Dart does not try to + # resolve the root workspace, which also contains a Flutter package. + run: dart tool/set_version.dart --check + + quality: + name: Run release quality checks + permissions: + contents: read + uses: ./.github/workflows/ci.yaml + + publish: + name: Publish insforge + needs: [validate, quality] + permissions: + id-token: write + uses: dart-lang/setup-dart/.github/workflows/publish.yml@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2 + with: + working-directory: packages/insforge + + release: + name: Create stable GitHub Release + if: needs.validate.outputs.stable == 'true' + needs: [validate, publish] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + REPOSITORY: ${{ github.repository }} + run: | + if gh release view "$RELEASE_TAG" --repo "$REPOSITORY" >/dev/null 2>&1; then + echo "GitHub Release $RELEASE_TAG already exists" + else + gh release create "$RELEASE_TAG" --repo "$REPOSITORY" --generate-notes --verify-tag + fi diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 270a02e..6a4518e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,12 +129,28 @@ apart. 1. `dart run tool/set_version.dart `. 2. Add a `## ` section to each package's `CHANGELOG.md`. 3. Commit, open a PR, merge to `main`. -4. Tag the release: `git tag v && git push --tags`. -5. Publish **in order** (the Flutter package depends on the Dart one): +4. Tag and publish the core package: ```bash - cd packages/insforge && dart pub publish # wait until it's live - cd ../insforge_flutter && dart pub publish + git tag -a insforge-v -m "insforge v" + git push origin insforge-v ``` +5. Wait until that exact `insforge` version is visible on pub.dev. +6. Tag and publish the Flutter package from the same release commit: + ```bash + git tag -a insforge_flutter-v \ + "insforge-v^{}" -m "insforge_flutter v" + git push origin insforge_flutter-v + ``` + + The `^{}` suffix peels the annotated core tag to its commit, so this remains + pinned to the core release even if the checked-out branch advances. + +Both tags trigger package-specific GitHub Actions workflows that publish through +pub.dev automated publishing (OIDC). No pub.dev token is stored in GitHub. Do +not push the `insforge_flutter` tag before the matching `insforge` version is +available, because the Flutter package depends on it. Each stable package tag +creates a GitHub Release after pub.dev publication succeeds; prerelease tags +publish to pub.dev without creating a GitHub Release. > Note on 0.x: under SemVer, `^0.1.0` means `>=0.1.0 <0.2.0`. Bumping the minor > (e.g. `0.1.x → 0.2.0`) is a breaking change, and the tool updates diff --git a/integration_tests/test/database_test.dart b/integration_tests/test/database_test.dart index ddad429..459768e 100644 --- a/integration_tests/test/database_test.dart +++ b/integration_tests/test/database_test.dart @@ -142,8 +142,16 @@ void main() { final rows = await db .from(_table) .insert(>[ - {'name': 'batch-a-$ts', 'value': 'batch', 'score': 20}, - {'name': 'batch-b-$ts', 'value': 'batch', 'score': 30}, + { + 'name': 'batch-a-$ts', + 'value': 'batch', + 'score': 20, + }, + { + 'name': 'batch-b-$ts', + 'value': 'batch', + 'score': 30, + }, ]) .select() .execute(); @@ -159,7 +167,9 @@ void main() { final tag = 'update-${DateTime.now().microsecondsSinceEpoch}'; final inserted = await db .from(_table) - .insert({'name': tag, 'value': 'before', 'score': 0}) + .insert( + {'name': tag, 'value': 'before', 'score': 0}, + ) .select() .execute(); final Object id = inserted.first['id'] as Object; @@ -207,8 +217,7 @@ void main() { await db.from(_table).eq('id', id).delete().execute(); - final check = - await db.from(_table).select('id').eq('id', id).execute(); + final check = await db.from(_table).select('id').eq('id', id).execute(); expect(check, isEmpty); }); @@ -222,14 +231,27 @@ void main() { final seeded = await db .from(_table) .insert(>[ - {'name': '$tag-alpha', 'value': 'hello world', 'score': 10}, - {'name': '$tag-beta', 'value': 'hello earth', 'score': 20}, - {'name': '$tag-gamma', 'value': 'goodbye world', 'score': 30}, + { + 'name': '$tag-alpha', + 'value': 'hello world', + 'score': 10, + }, + { + 'name': '$tag-beta', + 'value': 'hello earth', + 'score': 20, + }, + { + 'name': '$tag-gamma', + 'value': 'goodbye world', + 'score': 30, + }, ]) .select() .execute(); - seedIds = - seeded.map((Map r) => r['id'] as Object).toList(); + seedIds = seeded + .map((Map r) => r['id'] as Object) + .toList(); insertedIds.addAll(seedIds); }); @@ -310,8 +332,7 @@ void main() { final rows = await db .from(_table) .select('name') - .inFilter('name', ['$tag-alpha', '$tag-gamma']) - .execute(); + .inFilter('name', ['$tag-alpha', '$tag-gamma']).execute(); expect(rows.length, greaterThanOrEqualTo(2)); }); diff --git a/integration_tests/test/storage_test.dart b/integration_tests/test/storage_test.dart index b84be19..b22b6dc 100644 --- a/integration_tests/test/storage_test.dart +++ b/integration_tests/test/storage_test.dart @@ -99,7 +99,8 @@ void main() { test('uploadAutoKey returns a server-generated key', () async { if (!bucketAvailable) return; - final content = 'Auto upload – ${DateTime.now().microsecondsSinceEpoch}'; + final content = + 'Auto upload – ${DateTime.now().microsecondsSinceEpoch}'; final bytes = Uint8List.fromList(utf8.encode(content)); try { @@ -121,9 +122,9 @@ void main() { test('download of a non-existent object throws', () async { await expectLater( - storage - .from(_bucket) - .download('nonexistent-${DateTime.now().microsecondsSinceEpoch}.txt'), + storage.from(_bucket).download( + 'nonexistent-${DateTime.now().microsecondsSinceEpoch}.txt', + ), throwsA(isA()), ); }); diff --git a/packages/insforge/.pubignore b/packages/insforge/.pubignore index f095e20..3cfc49c 100644 --- a/packages/insforge/.pubignore +++ b/packages/insforge/.pubignore @@ -1,3 +1,5 @@ # Files excluded from the published package archive (not from git). -# Tests aren't needed by consumers and keep the package small. +# Local tooling, build artifacts, and tests aren't needed by consumers. +.dart_tool/ +build/ test/ diff --git a/packages/insforge/lib/src/ai/images_api.dart b/packages/insforge/lib/src/ai/images_api.dart index e048fcc..eae9ee5 100644 --- a/packages/insforge/lib/src/ai/images_api.dart +++ b/packages/insforge/lib/src/ai/images_api.dart @@ -45,8 +45,7 @@ class Images { if (rawImages is List) { for (final img in rawImages.whereType>()) { final imageUrl = img['image_url']; - final url = - imageUrl is Map ? imageUrl['url'] : null; + final url = imageUrl is Map ? imageUrl['url'] : null; if (url is String && url.isNotEmpty) { images.add(GeneratedImage.fromImageUrl(url)); } diff --git a/packages/insforge/lib/src/ai/models/chat_completion.dart b/packages/insforge/lib/src/ai/models/chat_completion.dart index d1b8ad7..ee3c7c4 100644 --- a/packages/insforge/lib/src/ai/models/chat_completion.dart +++ b/packages/insforge/lib/src/ai/models/chat_completion.dart @@ -103,8 +103,7 @@ class ChatCompletionResponse { final Usage? usage; /// Convenience accessor for the first choice's text content. - String? get content => - choices.isEmpty ? null : choices.first.message.content; + String? get content => choices.isEmpty ? null : choices.first.message.content; factory ChatCompletionResponse.fromJson(Map json) { final rawChoices = json['choices']; diff --git a/packages/insforge/lib/src/ai/models/embeddings.dart b/packages/insforge/lib/src/ai/models/embeddings.dart index df59830..a4a116e 100644 --- a/packages/insforge/lib/src/ai/models/embeddings.dart +++ b/packages/insforge/lib/src/ai/models/embeddings.dart @@ -10,7 +10,7 @@ class EmbeddingsRequest { required this.input, this.encodingFormat, this.dimensions, - }) : assert( + }) : assert( input is String || input is List, 'input must be a String or List', ); diff --git a/packages/insforge/lib/src/auth/auth_client.dart b/packages/insforge/lib/src/auth/auth_client.dart index 7f571a2..1b230e7 100644 --- a/packages/insforge/lib/src/auth/auth_client.dart +++ b/packages/insforge/lib/src/auth/auth_client.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:insforge/insforge.dart'; - /// Storage key for the persisted refresh token. const String kRefreshTokenKey = 'insforge_refresh_token'; diff --git a/packages/insforge/lib/src/auth/models/user.dart b/packages/insforge/lib/src/auth/models/user.dart index ffd5036..0511738 100644 --- a/packages/insforge/lib/src/auth/models/user.dart +++ b/packages/insforge/lib/src/auth/models/user.dart @@ -40,8 +40,7 @@ class User { providers: rawProviders is List ? rawProviders.map((dynamic e) => e.toString()).toList() : const [], - profile: - rawProfile is Map ? Map.from(rawProfile) : null, + profile: rawProfile is Map ? Map.from(rawProfile) : null, metadata: rawMetadata is Map ? Map.from(rawMetadata) : null, createdAt: parseInsforgeDate(json['createdAt'] as String?), diff --git a/packages/insforge/lib/src/core/http_client.dart b/packages/insforge/lib/src/core/http_client.dart index 52927cc..716fdfa 100644 --- a/packages/insforge/lib/src/core/http_client.dart +++ b/packages/insforge/lib/src/core/http_client.dart @@ -154,7 +154,9 @@ class InsforgeHttpClient { final data = response.data; final parsed = data is Map ? ErrorResponse.fromJson(data) - : ErrorResponse(message: data?.toString() ?? 'HTTP ${response.statusCode}'); + : ErrorResponse( + message: data?.toString() ?? 'HTTP ${response.statusCode}', + ); return InsforgeHttpException( statusCode: response.statusCode ?? -1, message: parsed.message, diff --git a/packages/insforge/lib/src/core/logging_interceptor.dart b/packages/insforge/lib/src/core/logging_interceptor.dart index 7badea6..94aac98 100644 --- a/packages/insforge/lib/src/core/logging_interceptor.dart +++ b/packages/insforge/lib/src/core/logging_interceptor.dart @@ -9,8 +9,7 @@ typedef LogSink = void Function(String message); /// Lightweight dio interceptor that logs requests/responses at the configured /// [LogLevel], redacting sensitive headers. class LoggingInterceptor extends Interceptor { - LoggingInterceptor(this.level, {LogSink? sink}) - : sink = sink ?? _noop; + LoggingInterceptor(this.level, {LogSink? sink}) : sink = sink ?? _noop; final LogLevel level; final LogSink sink; @@ -22,13 +21,18 @@ class LoggingInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (_enabled) { - sink('→ ${options.method} ${options.uri} headers=${_redact(options.headers)}'); + sink( + '→ ${options.method} ${options.uri} headers=${_redact(options.headers)}', + ); } handler.next(options); } @override - void onResponse(Response response, ResponseInterceptorHandler handler) { + void onResponse( + Response response, + ResponseInterceptorHandler handler, + ) { if (_enabled) { sink('← ${response.statusCode} ${response.requestOptions.uri}'); } @@ -38,7 +42,9 @@ class LoggingInterceptor extends Interceptor { @override void onError(DioException err, ErrorInterceptorHandler handler) { if (level.index >= LogLevel.error.index && level != LogLevel.none) { - sink('✗ ${err.response?.statusCode ?? '-'} ${err.requestOptions.uri}: ${err.message}'); + sink( + '✗ ${err.response?.statusCode ?? '-'} ${err.requestOptions.uri}: ${err.message}', + ); } handler.next(err); } diff --git a/packages/insforge/lib/src/database/database_client.dart b/packages/insforge/lib/src/database/database_client.dart index b51696f..255a581 100644 --- a/packages/insforge/lib/src/database/database_client.dart +++ b/packages/insforge/lib/src/database/database_client.dart @@ -1,7 +1,6 @@ // packages/insforge_database/lib/src/database_client.dart import 'package:insforge/insforge.dart'; - /// Entry point for PostgREST-style database access. /// /// Wraps a shared [InsforgeHttpClient]; all requests inherit its auth-header diff --git a/packages/insforge/lib/src/database/query_builder.dart b/packages/insforge/lib/src/database/query_builder.dart index aab6a1e..87b165f 100644 --- a/packages/insforge/lib/src/database/query_builder.dart +++ b/packages/insforge/lib/src/database/query_builder.dart @@ -1,7 +1,6 @@ // packages/insforge_database/lib/src/query_builder.dart import 'package:insforge/insforge.dart'; - /// Fluent, mutable builder for PostgREST-style record queries. /// /// Methods accumulate query parameters and return `this`, so calls chain on a @@ -193,7 +192,9 @@ class QueryBuilder { } /// Executes the query and maps each row with [fromJson]. - Future> executeAs(T Function(Map) fromJson) async { + Future> executeAs( + T Function(Map) fromJson, + ) async { final rows = await execute(); return rows.map(fromJson).toList(); } diff --git a/packages/insforge/lib/src/storage/mime.dart b/packages/insforge/lib/src/storage/mime.dart index effe1f3..ed860ab 100644 --- a/packages/insforge/lib/src/storage/mime.dart +++ b/packages/insforge/lib/src/storage/mime.dart @@ -25,8 +25,7 @@ const Map _extensionToMime = { /// case-insensitively. Returns the fallback when there is no extension. String contentTypeForFilename(String filename) { final lastSlash = filename.lastIndexOf('/'); - final segment = - lastSlash >= 0 ? filename.substring(lastSlash + 1) : filename; + final segment = lastSlash >= 0 ? filename.substring(lastSlash + 1) : filename; final dot = segment.lastIndexOf('.'); if (dot < 0 || dot == segment.length - 1) { return defaultContentType; diff --git a/packages/insforge/lib/src/storage/storage_client.dart b/packages/insforge/lib/src/storage/storage_client.dart index 69a8d95..1169779 100644 --- a/packages/insforge/lib/src/storage/storage_client.dart +++ b/packages/insforge/lib/src/storage/storage_client.dart @@ -1,7 +1,6 @@ // packages/insforge_storage/lib/src/storage_client.dart import 'package:insforge/insforge.dart'; - /// Entry point for InsForge storage: bucket administration plus per-bucket /// file operations via [from]. /// diff --git a/packages/insforge/lib/src/storage/storage_file_api.dart b/packages/insforge/lib/src/storage/storage_file_api.dart index c5b38df..e8aff5c 100644 --- a/packages/insforge/lib/src/storage/storage_file_api.dart +++ b/packages/insforge/lib/src/storage/storage_file_api.dart @@ -5,7 +5,6 @@ import 'package:dio/dio.dart'; import 'package:http_parser/http_parser.dart'; import 'package:insforge/insforge.dart'; - /// Operations on the objects of a single bucket. Obtain one via /// [StorageClient.from]. class StorageFileApi { diff --git a/packages/insforge/test/ai/chat_completions_stream_test.dart b/packages/insforge/test/ai/chat_completions_stream_test.dart index 1119b13..cc24c3a 100644 --- a/packages/insforge/test/ai/chat_completions_stream_test.dart +++ b/packages/insforge/test/ai/chat_completions_stream_test.dart @@ -6,8 +6,7 @@ import 'support/sse_adapter.dart'; void main() { group('ChatCompletions.createStream', () { - test('parses SSE chunks into content deltas and stops at [DONE]', - () async { + test('parses SSE chunks into content deltas and stops at [DONE]', () async { final dio = Dio(); dio.httpClientAdapter = SseAdapter([ '{"id":"c","model":"m","choices":[{"index":0,' diff --git a/packages/insforge/test/ai/support/recording_adapter.dart b/packages/insforge/test/ai/support/recording_adapter.dart index a645895..25ccf70 100644 --- a/packages/insforge/test/ai/support/recording_adapter.dart +++ b/packages/insforge/test/ai/support/recording_adapter.dart @@ -12,8 +12,7 @@ class RecordingAdapter implements HttpClientAdapter { final int statusCode; final List> bodies = >[]; - final List>> headers = - >>[]; + final List>> headers = >>[]; final List paths = []; @override diff --git a/packages/insforge/test/auth/auth_client_email_test.dart b/packages/insforge/test/auth/auth_client_email_test.dart index 51a02d1..7b627f6 100644 --- a/packages/insforge/test/auth/auth_client_email_test.dart +++ b/packages/insforge/test/auth/auth_client_email_test.dart @@ -85,7 +85,10 @@ void main() { (server) => server.reply(200, { 'message': 'Password reset successfully', }), - data: {'newPassword': 'newpw123', 'otp': 'reset-token-1'}, + data: { + 'newPassword': 'newpw123', + 'otp': 'reset-token-1', + }, ); await auth.resetPassword(otp: 'reset-token-1', newPassword: 'newpw123'); diff --git a/packages/insforge/test/auth/auth_client_oauth_test.dart b/packages/insforge/test/auth/auth_client_oauth_test.dart index d589e0c..81f9c64 100644 --- a/packages/insforge/test/auth/auth_client_oauth_test.dart +++ b/packages/insforge/test/auth/auth_client_oauth_test.dart @@ -56,7 +56,8 @@ void main() { queryParameters: {'client_type': 'mobile'}, ); - final callback = Uri.parse('myapp://callback?insforge_code=insforge-code-1'); + final callback = + Uri.parse('myapp://callback?insforge_code=insforge-code-1'); final response = await auth.handleOAuthCallback(callback, 'verifier-1'); expect(response.accessToken, 'oauth-access'); diff --git a/packages/insforge/test/auth/profile_test.dart b/packages/insforge/test/auth/profile_test.dart index a226129..3f82923 100644 --- a/packages/insforge/test/auth/profile_test.dart +++ b/packages/insforge/test/auth/profile_test.dart @@ -15,7 +15,8 @@ void main() { }); test('tolerates a null profile by yielding an empty map', () { - final p = Profile.fromJson({'id': 'u-2', 'profile': null}); + final p = + Profile.fromJson({'id': 'u-2', 'profile': null}); expect(p.id, 'u-2'); expect(p.profile, isEmpty); }); diff --git a/packages/insforge/test/core/http_client_auth_test.dart b/packages/insforge/test/core/http_client_auth_test.dart index 49b80a6..86bade7 100644 --- a/packages/insforge/test/core/http_client_auth_test.dart +++ b/packages/insforge/test/core/http_client_auth_test.dart @@ -7,7 +7,8 @@ import 'package:test/test.dart'; /// Records the headers of each request and returns a fixed JSON 200. class RecordingAdapter implements HttpClientAdapter { - final List>> requests = >>[]; + final List>> requests = + >>[]; @override Future fetch( @@ -15,9 +16,13 @@ class RecordingAdapter implements HttpClientAdapter { Stream? requestStream, Future? cancelFuture, ) async { - requests.add(Map>.from(options.headers.map( - (k, v) => MapEntry(k, [v.toString()]), - ),),); + requests.add( + Map>.from( + options.headers.map( + (k, v) => MapEntry(k, [v.toString()]), + ), + ), + ); return ResponseBody.fromString( '{"ok":true}', 200, @@ -42,7 +47,10 @@ void main() { await client.request('GET', '/api/database/records/posts'); - expect(adapter.requests.single['Authorization'], ['Bearer anon-123']); + expect( + adapter.requests.single['Authorization'], + ['Bearer anon-123'], + ); }); test('prefers the session access token over the anon key', () async { @@ -56,7 +64,10 @@ void main() { await client.request('GET', '/api/database/records/posts'); - expect(adapter.requests.single['Authorization'], ['Bearer user-jwt']); + expect( + adapter.requests.single['Authorization'], + ['Bearer user-jwt'], + ); }); test('adds x-api-key when configured', () async { diff --git a/packages/insforge/test/core/http_client_error_test.dart b/packages/insforge/test/core/http_client_error_test.dart index 2b6f18d..5628d2a 100644 --- a/packages/insforge/test/core/http_client_error_test.dart +++ b/packages/insforge/test/core/http_client_error_test.dart @@ -49,7 +49,11 @@ void main() { isA() .having((e) => e.statusCode, 'statusCode', 404) .having((e) => e.error, 'error', 'TABLE_NOT_FOUND') - .having((e) => e.nextActions, 'nextActions', 'Create the table first.'), + .having( + (e) => e.nextActions, + 'nextActions', + 'Create the table first.', + ), ), ); }); diff --git a/packages/insforge/test/core/http_client_refresh_test.dart b/packages/insforge/test/core/http_client_refresh_test.dart index fdc30b4..8bb68c6 100644 --- a/packages/insforge/test/core/http_client_refresh_test.dart +++ b/packages/insforge/test/core/http_client_refresh_test.dart @@ -79,8 +79,10 @@ void main() { expect( () => client.request('POST', '/api/auth/refresh'), - throwsA(isA() - .having((e) => e.statusCode, 'statusCode', 401),), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 401), + ), ); }); } diff --git a/packages/insforge/test/core/url_test.dart b/packages/insforge/test/core/url_test.dart index 40ff184..6bf6343 100644 --- a/packages/insforge/test/core/url_test.dart +++ b/packages/insforge/test/core/url_test.dart @@ -9,7 +9,10 @@ void main() { }); test('keeps an explicit http scheme', () { - expect(normalizeBaseUrl('http://localhost:7130'), 'http://localhost:7130'); + expect( + normalizeBaseUrl('http://localhost:7130'), + 'http://localhost:7130', + ); }); test('uses http when useHttps is false and no scheme given', () { diff --git a/packages/insforge/test/database/query_filters_test.dart b/packages/insforge/test/database/query_filters_test.dart index 4f5b782..4c2dec4 100644 --- a/packages/insforge/test/database/query_filters_test.dart +++ b/packages/insforge/test/database/query_filters_test.dart @@ -66,7 +66,8 @@ void main() { }); }); - test('a single filter on a column stays a bare string (not a list)', () async { + test('a single filter on a column stays a bare string (not a list)', + () async { final adapter = RecordingAdapter(responseBody: []); final db = DatabaseClient(_client(adapter)); @@ -95,7 +96,8 @@ void main() { expect(adapter.single.queryParameters['id'], 'in.(1,2,3)'); }); - test('contains/containedBy/or/not/filter/textSearch escape hatches', () async { + test('contains/containedBy/or/not/filter/textSearch escape hatches', + () async { final adapter = RecordingAdapter(responseBody: []); final db = DatabaseClient(_client(adapter)); diff --git a/packages/insforge/test/database/rpc_test.dart b/packages/insforge/test/database/rpc_test.dart index d857af7..dab80bd 100644 --- a/packages/insforge/test/database/rpc_test.dart +++ b/packages/insforge/test/database/rpc_test.dart @@ -37,9 +37,10 @@ void main() { ); final db = DatabaseClient(_client(adapter)); - final result = await db - .rpc('get_user_stats', args: {'user_id': 123}) - .execute(); + final result = await db.rpc( + 'get_user_stats', + args: {'user_id': 123}, + ).execute(); final req = adapter.single; expect(req.method, 'POST'); diff --git a/packages/insforge/test/insforge_client_test.dart b/packages/insforge/test/insforge_client_test.dart index 457f648..5b89113 100644 --- a/packages/insforge/test/insforge_client_test.dart +++ b/packages/insforge/test/insforge_client_test.dart @@ -34,7 +34,8 @@ void main() { expect(identical(client.http, client.http), isTrue); }); - test('ai getter throws a clear error when no OpenRouter key was supplied', () { + test('ai getter throws a clear error when no OpenRouter key was supplied', + () { final client = build(); expect( () => client.ai, diff --git a/packages/insforge_flutter/.pubignore b/packages/insforge_flutter/.pubignore index f095e20..3cfc49c 100644 --- a/packages/insforge_flutter/.pubignore +++ b/packages/insforge_flutter/.pubignore @@ -1,3 +1,5 @@ # Files excluded from the published package archive (not from git). -# Tests aren't needed by consumers and keep the package small. +# Local tooling, build artifacts, and tests aren't needed by consumers. +.dart_tool/ +build/ test/ diff --git a/tool/set_version.dart b/tool/set_version.dart index e2dd7bf..cc6c506 100644 --- a/tool/set_version.dart +++ b/tool/set_version.dart @@ -18,12 +18,15 @@ const String _insforgePubspec = 'packages/insforge/pubspec.yaml'; const String _flutterPubspec = 'packages/insforge_flutter/pubspec.yaml'; const String _versionDart = 'packages/insforge/lib/src/core/version.dart'; -final RegExp _semver = RegExp(r'^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$'); +final RegExp _semver = RegExp( + r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)' + r'(?:-(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)' + r'(?:\.(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*)?' + r'(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$', +); final RegExp _versionLine = RegExp(r'^version:.*$', multiLine: true); -final RegExp _depLine = - RegExp(r'^(\s+insforge:\s*\^).*$', multiLine: true); -final RegExp _dartConst = - RegExp(r"const String insforgeSdkVersion = '.*';"); +final RegExp _depLine = RegExp(r'^(\s+insforge:\s*\^).*$', multiLine: true); +final RegExp _dartConst = RegExp(r"const String insforgeSdkVersion = '.*';"); void main(List args) { _ensureRepoRoot(); @@ -35,7 +38,8 @@ void main(List args) { } final version = args.first; if (!_semver.hasMatch(version)) { - stderr.writeln('Invalid version "$version" (expected X.Y.Z[-pre][+build]).'); + stderr + .writeln('Invalid version "$version" (expected X.Y.Z[-pre][+build]).'); exit(64); } _set(version); @@ -57,8 +61,9 @@ void _set(String version) { stdout.writeln('Set insforge + insforge_flutter to $version ' '(and insforge_flutter -> insforge: ^$version).'); stdout.writeln('Regenerated $_versionDart.'); - stdout.writeln('Next: update each package CHANGELOG.md, commit, tag ' - 'v$version, then publish insforge before insforge_flutter.'); + stdout.writeln('Next: update each package CHANGELOG.md and commit.'); + stdout.writeln('Then push insforge-v$version, wait for it on pub.dev, and ' + 'push insforge_flutter-v$version from the same commit.'); } bool _check() { @@ -72,12 +77,13 @@ bool _check() { 'insforge_flutter version ($flutter) != insforge version ($core)', if (dart != core) 'version.dart insforgeSdkVersion ($dart) != insforge version ($core)', - if (dep != core) - 'insforge_flutter dep "insforge: ^$dep" != ^$core', + if (dep != core) 'insforge_flutter dep "insforge: ^$dep" != ^$core', ]; if (problems.isEmpty) { - stdout.writeln('Version $core is in sync across both packages + version.dart.'); + stdout.writeln( + 'Version $core is in sync across both packages + version.dart.', + ); return true; } stderr.writeln('Version drift detected:');