Skip to content

Commit bd949d9

Browse files
committed
feat(core): versioned User-Agent + lockstep version tooling
- Add a generated version.dart (insforgeSdkVersion + insforgeUserAgent = 'InsForge-Dart/<version>'); both the HTTP client and the AI client now send it, mirroring the insforge-kotlin InsforgeVersion approach. - tool/set_version.dart keeps insforge + insforge_flutter in lockstep (version, the insforge_flutter -> insforge dep constraint, and version.dart) from one command, with --check used by CI to prevent drift. - Document the release process in CONTRIBUTING; lock the User-Agent with a test.
1 parent f1d2ecf commit bd949d9

8 files changed

Lines changed: 222 additions & 6 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
run: flutter pub get
2222
- name: Analyze
2323
run: dart analyze .
24+
- name: Check version is in sync (both packages + version.dart)
25+
run: dart run tool/set_version.dart --check
2426
- name: Test insforge (pure Dart)
2527
working-directory: packages/insforge
2628
run: dart test

CONTRIBUTING.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,45 @@ docs(readme): document the AI module
102102
Scopes are typically a module name (`auth`, `database`, `storage`,
103103
`functions`, `ai`, `core`) or `sample` / `integration` / `ci`.
104104

105+
## Versioning & releasing
106+
107+
`insforge` and `insforge_flutter` are released **in lockstep** — they always
108+
share the same version number. The version lives in three places that must stay
109+
in sync:
110+
111+
- `packages/insforge/pubspec.yaml``version:`
112+
- `packages/insforge_flutter/pubspec.yaml``version:` **and** its
113+
`insforge: ^<version>` dependency constraint
114+
- `packages/insforge/lib/src/core/version.dart``insforgeSdkVersion`
115+
(a generated constant; the runtime `User-Agent` is `InsForge-Dart/<version>`)
116+
117+
Don't edit these by hand — use the tool, which updates all of them at once:
118+
119+
```bash
120+
dart run tool/set_version.dart 0.2.0 # set the version everywhere
121+
dart run tool/set_version.dart --check # verify they're in sync (CI runs this)
122+
```
123+
124+
`version.dart` is generated; never edit it directly. CI fails if the three drift
125+
apart.
126+
127+
### Release checklist
128+
129+
1. `dart run tool/set_version.dart <new-version>`.
130+
2. Add a `## <new-version>` section to each package's `CHANGELOG.md`.
131+
3. Commit, open a PR, merge to `main`.
132+
4. Tag the release: `git tag v<new-version> && git push --tags`.
133+
5. Publish **in order** (the Flutter package depends on the Dart one):
134+
```bash
135+
cd packages/insforge && dart pub publish # wait until it's live
136+
cd ../insforge_flutter && dart pub publish
137+
```
138+
139+
> Note on 0.x: under SemVer, `^0.1.0` means `>=0.1.0 <0.2.0`. Bumping the minor
140+
> (e.g. `0.1.x → 0.2.0`) is a breaking change, and the tool updates
141+
> `insforge_flutter`'s `insforge: ^…` constraint accordingly so it picks up the
142+
> new core.
143+
105144
## Pull requests
106145

107146
1. Branch off `main`.

packages/insforge/lib/insforge.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export 'src/core/logging_interceptor.dart';
1010
export 'src/core/options.dart';
1111
export 'src/core/session_storage.dart';
1212
export 'src/core/url.dart';
13+
export 'src/core/version.dart';
1314

1415
// --- auth ---
1516
export 'src/auth/auth_client.dart';

packages/insforge/lib/src/ai/ai_client.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:dio/dio.dart';
22

3+
import '../core/version.dart';
34
import 'chat_completions.dart';
45
import 'embeddings_api.dart';
56
import 'images_api.dart';
@@ -8,8 +9,6 @@ import 'models_api.dart';
89
/// Default OpenRouter API base URL.
910
const String openRouterBaseUrl = 'https://openrouter.ai/api/v1';
1011

11-
const String _userAgent = 'insforge-flutter-ai/0.1.0';
12-
1312
/// Groups the chat sub-namespace (`client.chat.completions`).
1413
class Chat {
1514
Chat(Dio dio) : completions = ChatCompletions(dio);
@@ -34,7 +33,7 @@ class AIClient {
3433
..baseUrl = baseUrl
3534
..headers = <String, dynamic>{
3635
'Authorization': 'Bearer $apiKey',
37-
'User-Agent': _userAgent,
36+
'User-Agent': insforgeUserAgent,
3837
if (referer != null) 'HTTP-Referer': referer,
3938
if (title != null) 'X-Title': title,
4039
...this.dio.options.headers,

packages/insforge/lib/src/core/http_client.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
// packages/insforge_core/lib/src/http_client.dart
1+
// packages/insforge/lib/src/core/http_client.dart
22
import 'package:dio/dio.dart';
33

44
import 'error_response.dart';
55
import 'errors.dart';
66
import 'logging_interceptor.dart';
77
import 'options.dart';
88
import 'url.dart';
9+
import 'version.dart';
910

1011
/// Supplies an access token on demand (e.g. from an external auth provider).
1112
typedef AccessTokenProvider = String? Function();
1213

1314
/// Performs a token refresh and returns the new access token. Throws on failure.
1415
typedef RefreshCallback = Future<String> Function();
1516

16-
const String insforgeUserAgent = 'insforge-flutter/0.1.0';
17-
1817
/// Shared HTTP transport for all InsForge SDK modules.
1918
///
2019
/// Wraps a single [Dio]. Injects `Authorization`/`x-api-key` headers per
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// packages/insforge/lib/src/core/version.dart
2+
//
3+
// GENERATED FILE — do not edit by hand.
4+
// Mirrors `packages/insforge/pubspec.yaml`'s `version:`. Regenerate (and keep
5+
// both packages in lockstep) with: dart run tool/set_version.dart <version>
6+
7+
/// The current InsForge Dart SDK version (mirrors `pubspec.yaml`).
8+
const String insforgeSdkVersion = '0.1.0';
9+
10+
/// `User-Agent` sent on every request from this SDK: `InsForge-Dart/<version>`.
11+
const String insforgeUserAgent = 'InsForge-Dart/$insforgeSdkVersion';

packages/insforge/test/core/http_client_auth_test.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,19 @@ void main() {
7272

7373
expect(adapter.requests.single['x-api-key'], <String>['apikey-xyz']);
7474
});
75+
76+
test('sends the versioned User-Agent (InsForge-Dart/<version>)', () async {
77+
final adapter = RecordingAdapter();
78+
final client = InsforgeHttpClient(
79+
baseUrl: 'https://x.insforge.app',
80+
anonKey: 'anon-123',
81+
);
82+
client.dio.httpClientAdapter = adapter;
83+
84+
await client.request<dynamic>('GET', '/api/database/records/posts');
85+
86+
expect(adapter.requests.single['User-Agent'], <String>[insforgeUserAgent]);
87+
expect(insforgeUserAgent, 'InsForge-Dart/$insforgeSdkVersion');
88+
expect(insforgeUserAgent, startsWith('InsForge-Dart/'));
89+
});
7590
}

tool/set_version.dart

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// tool/set_version.dart
2+
//
3+
// Single source of truth for the SDK version. Keeps `insforge` and
4+
// `insforge_flutter` in lockstep and regenerates the runtime version constant
5+
// used for the `User-Agent` header.
6+
//
7+
// Usage (run from the repo root):
8+
// dart run tool/set_version.dart 0.2.0 # set the version everywhere
9+
// dart run tool/set_version.dart --check # verify everything is in sync (CI)
10+
//
11+
// "Set" updates, all to the same version:
12+
// * packages/insforge/pubspec.yaml version:
13+
// * packages/insforge_flutter/pubspec.yaml version: and insforge: ^<v>
14+
// * packages/insforge/lib/src/core/version.dart insforgeSdkVersion
15+
import 'dart:io';
16+
17+
const String _insforgePubspec = 'packages/insforge/pubspec.yaml';
18+
const String _flutterPubspec = 'packages/insforge_flutter/pubspec.yaml';
19+
const String _versionDart = 'packages/insforge/lib/src/core/version.dart';
20+
21+
final RegExp _semver = RegExp(r'^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$');
22+
final RegExp _versionLine = RegExp(r'^version:.*$', multiLine: true);
23+
final RegExp _depLine =
24+
RegExp(r'^(\s+insforge:\s*\^).*$', multiLine: true);
25+
final RegExp _dartConst =
26+
RegExp(r"const String insforgeSdkVersion = '.*';");
27+
28+
void main(List<String> args) {
29+
_ensureRepoRoot();
30+
if (args.isEmpty) {
31+
_usageAndExit();
32+
}
33+
if (args.first == '--check') {
34+
exit(_check() ? 0 : 1);
35+
}
36+
final version = args.first;
37+
if (!_semver.hasMatch(version)) {
38+
stderr.writeln('Invalid version "$version" (expected X.Y.Z[-pre][+build]).');
39+
exit(64);
40+
}
41+
_set(version);
42+
}
43+
44+
void _set(String version) {
45+
_replaceFirst(_insforgePubspec, _versionLine, 'version: $version');
46+
_replaceFirst(_flutterPubspec, _versionLine, 'version: $version');
47+
_replaceFirstMapped(
48+
_flutterPubspec,
49+
_depLine,
50+
(Match m) => '${m.group(1)}$version',
51+
);
52+
_replaceFirst(
53+
_versionDart,
54+
_dartConst,
55+
"const String insforgeSdkVersion = '$version';",
56+
);
57+
stdout.writeln('Set insforge + insforge_flutter to $version '
58+
'(and insforge_flutter -> insforge: ^$version).');
59+
stdout.writeln('Regenerated $_versionDart.');
60+
stdout.writeln('Next: update each package CHANGELOG.md, commit, tag '
61+
'v$version, then publish insforge before insforge_flutter.');
62+
}
63+
64+
bool _check() {
65+
final core = _pubspecVersion(_insforgePubspec);
66+
final flutter = _pubspecVersion(_flutterPubspec);
67+
final dart = _dartVersion();
68+
final dep = _depConstraint();
69+
70+
final problems = <String>[
71+
if (flutter != core)
72+
'insforge_flutter version ($flutter) != insforge version ($core)',
73+
if (dart != core)
74+
'version.dart insforgeSdkVersion ($dart) != insforge version ($core)',
75+
if (dep != core)
76+
'insforge_flutter dep "insforge: ^$dep" != ^$core',
77+
];
78+
79+
if (problems.isEmpty) {
80+
stdout.writeln('Version $core is in sync across both packages + version.dart.');
81+
return true;
82+
}
83+
stderr.writeln('Version drift detected:');
84+
for (final p in problems) {
85+
stderr.writeln(' - $p');
86+
}
87+
stderr.writeln('Fix with: dart run tool/set_version.dart $core');
88+
return false;
89+
}
90+
91+
// --- file helpers ---
92+
93+
void _replaceFirst(String path, RegExp re, String replacement) {
94+
final file = File(path);
95+
final content = file.readAsStringSync();
96+
if (!re.hasMatch(content)) {
97+
stderr.writeln('Pattern ${re.pattern} not found in $path');
98+
exit(70);
99+
}
100+
file.writeAsStringSync(content.replaceFirst(re, replacement));
101+
}
102+
103+
void _replaceFirstMapped(
104+
String path,
105+
RegExp re,
106+
String Function(Match) replace,
107+
) {
108+
final file = File(path);
109+
final content = file.readAsStringSync();
110+
if (!re.hasMatch(content)) {
111+
stderr.writeln('Pattern ${re.pattern} not found in $path');
112+
exit(70);
113+
}
114+
file.writeAsStringSync(content.replaceFirstMapped(re, replace));
115+
}
116+
117+
String _pubspecVersion(String path) =>
118+
_firstGroup(path, RegExp(r'^version:\s*(.+?)\s*$', multiLine: true));
119+
120+
String _dartVersion() =>
121+
_firstGroup(_versionDart, RegExp(r"insforgeSdkVersion = '(.+?)'"));
122+
123+
String _depConstraint() => _firstGroup(
124+
_flutterPubspec,
125+
RegExp(r'^\s+insforge:\s*\^(.+?)\s*$', multiLine: true),
126+
);
127+
128+
String _firstGroup(String path, RegExp re) {
129+
final m = re.firstMatch(File(path).readAsStringSync());
130+
if (m == null) {
131+
stderr.writeln('Could not read ${re.pattern} from $path');
132+
exit(70);
133+
}
134+
return m.group(1)!;
135+
}
136+
137+
void _ensureRepoRoot() {
138+
if (!File(_insforgePubspec).existsSync()) {
139+
stderr.writeln('Run this from the repository root '
140+
'(could not find $_insforgePubspec).');
141+
exit(66);
142+
}
143+
}
144+
145+
void _usageAndExit() {
146+
stdout.writeln('Usage:');
147+
stdout.writeln(' dart run tool/set_version.dart <X.Y.Z> set the version');
148+
stdout.writeln(' dart run tool/set_version.dart --check verify sync (CI)');
149+
exit(64);
150+
}

0 commit comments

Comments
 (0)