Skip to content

Commit 3b2807e

Browse files
Merge pull request #4 from InsForge/codex/add-pubdev-publishing
ci: publish Dart packages with OIDC
2 parents 6b33f9c + 9e9917f commit 3b2807e

34 files changed

Lines changed: 341 additions & 79 deletions

.github/dependabot.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: github-actions
4+
directory: /
5+
schedule:
6+
interval: weekly

.github/workflows/ci.yaml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
push:
66
branches: [main]
77
pull_request:
8+
workflow_call:
89

910
jobs:
1011
# The workspace contains a Flutter package (packages/insforge_flutter), so the
@@ -13,12 +14,14 @@ jobs:
1314
analyze-and-test:
1415
runs-on: ubuntu-latest
1516
steps:
16-
- uses: actions/checkout@v4
17-
- uses: subosito/flutter-action@v2
17+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
18+
- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
1819
with:
1920
channel: stable
2021
- name: Install dependencies
2122
run: flutter pub get
23+
- name: Check formatting
24+
run: dart format --output=none --set-exit-if-changed .
2225
- name: Analyze
2326
run: dart analyze .
2427
- name: Check version is in sync (both packages + version.dart)
@@ -32,3 +35,9 @@ jobs:
3235
- name: Test insforge_flutter
3336
working-directory: packages/insforge_flutter
3437
run: flutter test
38+
- name: Dry-run publish insforge
39+
working-directory: packages/insforge
40+
run: dart pub publish --dry-run
41+
- name: Dry-run publish insforge_flutter
42+
working-directory: packages/insforge_flutter
43+
run: dart pub publish --dry-run
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Publish insforge_flutter to pub.dev
2+
3+
on:
4+
push:
5+
tags:
6+
- "insforge_flutter-v[0-9]+.[0-9]+.[0-9]+"
7+
- "insforge_flutter-v[0-9]+.[0-9]+.[0-9]+-*"
8+
- 'insforge_flutter-v[0-9]+.[0-9]+.[0-9]+\+*'
9+
10+
jobs:
11+
validate:
12+
name: Validate release
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
outputs:
17+
stable: ${{ steps.version.outputs.stable }}
18+
steps:
19+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
20+
21+
- uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2
22+
23+
- name: Validate tag matches package version
24+
id: version
25+
env:
26+
RELEASE_TAG: ${{ github.ref_name }}
27+
run: |
28+
version="$(sed -n 's/^version:[[:space:]]*//p' packages/insforge_flutter/pubspec.yaml)"
29+
if [ "$RELEASE_TAG" != "insforge_flutter-v$version" ]; then
30+
echo "Tag $RELEASE_TAG does not match insforge_flutter version $version."
31+
exit 1
32+
fi
33+
34+
echo "version=$version" >> "$GITHUB_OUTPUT"
35+
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
36+
echo "stable=true" >> "$GITHUB_OUTPUT"
37+
else
38+
echo "stable=false" >> "$GITHUB_OUTPUT"
39+
fi
40+
41+
- name: Check lockstep versions
42+
# Run the dependency-free script directly so Dart does not try to
43+
# resolve the root workspace, which also contains a Flutter package.
44+
run: dart tool/set_version.dart --check
45+
46+
- name: Verify matching insforge version is published
47+
env:
48+
RELEASE_VERSION: ${{ steps.version.outputs.version }}
49+
run: |
50+
if ! curl --fail --silent --show-error --retry 5 --retry-delay 10 \
51+
"https://pub.dev/api/packages/insforge/versions/$RELEASE_VERSION" \
52+
>/dev/null; then
53+
echo "insforge $RELEASE_VERSION is not available on pub.dev yet." >&2
54+
echo "Publish insforge-v$RELEASE_VERSION first and wait for it to become available." >&2
55+
exit 1
56+
fi
57+
58+
quality:
59+
name: Run release quality checks
60+
permissions:
61+
contents: read
62+
uses: ./.github/workflows/ci.yaml
63+
64+
publish:
65+
name: Publish insforge_flutter
66+
needs: [validate, quality]
67+
permissions:
68+
id-token: write
69+
uses: dart-lang/setup-dart/.github/workflows/publish.yml@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2
70+
with:
71+
working-directory: packages/insforge_flutter
72+
73+
release:
74+
name: Create stable GitHub Release
75+
if: needs.validate.outputs.stable == 'true'
76+
needs: [validate, publish]
77+
runs-on: ubuntu-latest
78+
permissions:
79+
contents: write
80+
81+
steps:
82+
- name: Create GitHub Release
83+
env:
84+
GH_TOKEN: ${{ github.token }}
85+
RELEASE_TAG: ${{ github.ref_name }}
86+
REPOSITORY: ${{ github.repository }}
87+
run: |
88+
if gh release view "$RELEASE_TAG" --repo "$REPOSITORY" >/dev/null 2>&1; then
89+
echo "GitHub Release $RELEASE_TAG already exists"
90+
else
91+
gh release create "$RELEASE_TAG" --repo "$REPOSITORY" --generate-notes --verify-tag
92+
fi
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Publish insforge to pub.dev
2+
3+
on:
4+
push:
5+
tags:
6+
- "insforge-v[0-9]+.[0-9]+.[0-9]+"
7+
- "insforge-v[0-9]+.[0-9]+.[0-9]+-*"
8+
- 'insforge-v[0-9]+.[0-9]+.[0-9]+\+*'
9+
10+
jobs:
11+
validate:
12+
name: Validate release
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
outputs:
17+
stable: ${{ steps.version.outputs.stable }}
18+
steps:
19+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
20+
21+
- uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2
22+
23+
- name: Validate tag matches package version
24+
id: version
25+
env:
26+
RELEASE_TAG: ${{ github.ref_name }}
27+
run: |
28+
version="$(sed -n 's/^version:[[:space:]]*//p' packages/insforge/pubspec.yaml)"
29+
if [ "$RELEASE_TAG" != "insforge-v$version" ]; then
30+
echo "Tag $RELEASE_TAG does not match insforge version $version."
31+
exit 1
32+
fi
33+
34+
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
35+
echo "stable=true" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "stable=false" >> "$GITHUB_OUTPUT"
38+
fi
39+
40+
- name: Check lockstep versions
41+
# Run the dependency-free script directly so Dart does not try to
42+
# resolve the root workspace, which also contains a Flutter package.
43+
run: dart tool/set_version.dart --check
44+
45+
quality:
46+
name: Run release quality checks
47+
permissions:
48+
contents: read
49+
uses: ./.github/workflows/ci.yaml
50+
51+
publish:
52+
name: Publish insforge
53+
needs: [validate, quality]
54+
permissions:
55+
id-token: write
56+
uses: dart-lang/setup-dart/.github/workflows/publish.yml@65eb853c7ba17dde3be364c3d2858773e7144260 # v1.7.2
57+
with:
58+
working-directory: packages/insforge
59+
60+
release:
61+
name: Create stable GitHub Release
62+
if: needs.validate.outputs.stable == 'true'
63+
needs: [validate, publish]
64+
runs-on: ubuntu-latest
65+
permissions:
66+
contents: write
67+
68+
steps:
69+
- name: Create GitHub Release
70+
env:
71+
GH_TOKEN: ${{ github.token }}
72+
RELEASE_TAG: ${{ github.ref_name }}
73+
REPOSITORY: ${{ github.repository }}
74+
run: |
75+
if gh release view "$RELEASE_TAG" --repo "$REPOSITORY" >/dev/null 2>&1; then
76+
echo "GitHub Release $RELEASE_TAG already exists"
77+
else
78+
gh release create "$RELEASE_TAG" --repo "$REPOSITORY" --generate-notes --verify-tag
79+
fi

CONTRIBUTING.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,28 @@ apart.
129129
1. `dart run tool/set_version.dart <new-version>`.
130130
2. Add a `## <new-version>` section to each package's `CHANGELOG.md`.
131131
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):
132+
4. Tag and publish the core package:
134133
```bash
135-
cd packages/insforge && dart pub publish # wait until it's live
136-
cd ../insforge_flutter && dart pub publish
134+
git tag -a insforge-v<new-version> -m "insforge v<new-version>"
135+
git push origin insforge-v<new-version>
137136
```
137+
5. Wait until that exact `insforge` version is visible on pub.dev.
138+
6. Tag and publish the Flutter package from the same release commit:
139+
```bash
140+
git tag -a insforge_flutter-v<new-version> \
141+
"insforge-v<new-version>^{}" -m "insforge_flutter v<new-version>"
142+
git push origin insforge_flutter-v<new-version>
143+
```
144+
145+
The `^{}` suffix peels the annotated core tag to its commit, so this remains
146+
pinned to the core release even if the checked-out branch advances.
147+
148+
Both tags trigger package-specific GitHub Actions workflows that publish through
149+
pub.dev automated publishing (OIDC). No pub.dev token is stored in GitHub. Do
150+
not push the `insforge_flutter` tag before the matching `insforge` version is
151+
available, because the Flutter package depends on it. Each stable package tag
152+
creates a GitHub Release after pub.dev publication succeeds; prerelease tags
153+
publish to pub.dev without creating a GitHub Release.
138154

139155
> Note on 0.x: under SemVer, `^0.1.0` means `>=0.1.0 <0.2.0`. Bumping the minor
140156
> (e.g. `0.1.x → 0.2.0`) is a breaking change, and the tool updates

integration_tests/test/database_test.dart

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,16 @@ void main() {
142142
final rows = await db
143143
.from(_table)
144144
.insert(<Map<String, dynamic>>[
145-
<String, dynamic>{'name': 'batch-a-$ts', 'value': 'batch', 'score': 20},
146-
<String, dynamic>{'name': 'batch-b-$ts', 'value': 'batch', 'score': 30},
145+
<String, dynamic>{
146+
'name': 'batch-a-$ts',
147+
'value': 'batch',
148+
'score': 20,
149+
},
150+
<String, dynamic>{
151+
'name': 'batch-b-$ts',
152+
'value': 'batch',
153+
'score': 30,
154+
},
147155
])
148156
.select()
149157
.execute();
@@ -159,7 +167,9 @@ void main() {
159167
final tag = 'update-${DateTime.now().microsecondsSinceEpoch}';
160168
final inserted = await db
161169
.from(_table)
162-
.insert(<String, dynamic>{'name': tag, 'value': 'before', 'score': 0})
170+
.insert(
171+
<String, dynamic>{'name': tag, 'value': 'before', 'score': 0},
172+
)
163173
.select()
164174
.execute();
165175
final Object id = inserted.first['id'] as Object;
@@ -207,8 +217,7 @@ void main() {
207217

208218
await db.from(_table).eq('id', id).delete().execute();
209219

210-
final check =
211-
await db.from(_table).select('id').eq('id', id).execute();
220+
final check = await db.from(_table).select('id').eq('id', id).execute();
212221
expect(check, isEmpty);
213222
});
214223

@@ -222,14 +231,27 @@ void main() {
222231
final seeded = await db
223232
.from(_table)
224233
.insert(<Map<String, dynamic>>[
225-
<String, dynamic>{'name': '$tag-alpha', 'value': 'hello world', 'score': 10},
226-
<String, dynamic>{'name': '$tag-beta', 'value': 'hello earth', 'score': 20},
227-
<String, dynamic>{'name': '$tag-gamma', 'value': 'goodbye world', 'score': 30},
234+
<String, dynamic>{
235+
'name': '$tag-alpha',
236+
'value': 'hello world',
237+
'score': 10,
238+
},
239+
<String, dynamic>{
240+
'name': '$tag-beta',
241+
'value': 'hello earth',
242+
'score': 20,
243+
},
244+
<String, dynamic>{
245+
'name': '$tag-gamma',
246+
'value': 'goodbye world',
247+
'score': 30,
248+
},
228249
])
229250
.select()
230251
.execute();
231-
seedIds =
232-
seeded.map((Map<String, dynamic> r) => r['id'] as Object).toList();
252+
seedIds = seeded
253+
.map((Map<String, dynamic> r) => r['id'] as Object)
254+
.toList();
233255
insertedIds.addAll(seedIds);
234256
});
235257

@@ -310,8 +332,7 @@ void main() {
310332
final rows = await db
311333
.from(_table)
312334
.select('name')
313-
.inFilter('name', <Object>['$tag-alpha', '$tag-gamma'])
314-
.execute();
335+
.inFilter('name', <Object>['$tag-alpha', '$tag-gamma']).execute();
315336
expect(rows.length, greaterThanOrEqualTo(2));
316337
});
317338

integration_tests/test/storage_test.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ void main() {
9999
test('uploadAutoKey returns a server-generated key', () async {
100100
if (!bucketAvailable) return;
101101

102-
final content = 'Auto upload – ${DateTime.now().microsecondsSinceEpoch}';
102+
final content =
103+
'Auto upload – ${DateTime.now().microsecondsSinceEpoch}';
103104
final bytes = Uint8List.fromList(utf8.encode(content));
104105

105106
try {
@@ -121,9 +122,9 @@ void main() {
121122

122123
test('download of a non-existent object throws', () async {
123124
await expectLater(
124-
storage
125-
.from(_bucket)
126-
.download('nonexistent-${DateTime.now().microsecondsSinceEpoch}.txt'),
125+
storage.from(_bucket).download(
126+
'nonexistent-${DateTime.now().microsecondsSinceEpoch}.txt',
127+
),
127128
throwsA(isA<InsforgeHttpException>()),
128129
);
129130
});

packages/insforge/.pubignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# Files excluded from the published package archive (not from git).
2-
# Tests aren't needed by consumers and keep the package small.
2+
# Local tooling, build artifacts, and tests aren't needed by consumers.
3+
.dart_tool/
4+
build/
35
test/

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ class Images {
4545
if (rawImages is List) {
4646
for (final img in rawImages.whereType<Map<String, dynamic>>()) {
4747
final imageUrl = img['image_url'];
48-
final url =
49-
imageUrl is Map<String, dynamic> ? imageUrl['url'] : null;
48+
final url = imageUrl is Map<String, dynamic> ? imageUrl['url'] : null;
5049
if (url is String && url.isNotEmpty) {
5150
images.add(GeneratedImage.fromImageUrl(url));
5251
}

packages/insforge/lib/src/ai/models/chat_completion.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ class ChatCompletionResponse {
103103
final Usage? usage;
104104

105105
/// Convenience accessor for the first choice's text content.
106-
String? get content =>
107-
choices.isEmpty ? null : choices.first.message.content;
106+
String? get content => choices.isEmpty ? null : choices.first.message.content;
108107

109108
factory ChatCompletionResponse.fromJson(Map<String, dynamic> json) {
110109
final rawChoices = json['choices'];

0 commit comments

Comments
 (0)