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
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,14 @@ Each action's `action.yml` documents every input; the most useful knobs:
`s3-endpoint` (MinIO). Outputs: `s3-url` (presigned), `s3-key`.
- **`codegen`** — `project-dir`, `api-dir`, `codegen-tool-dir`, `sdk-floor`,
`clean`, `api-pubspec-template`. Tuned for an OpenAPI-generated client + a
layered `build_runner`; point the dirs at your layout.
layered `build_runner`; point the dirs at your layout. `generate-openapi`
(`auto`/`true`/`false`, default `auto`) controls the OpenAPI-client stage
(steps 1–3 below) — `auto` skips it when `codegen-tool-dir` doesn't exist on
disk, so a consumer with no generated OpenAPI client (or one that dropped it
entirely) gets just the app-level `build_runner` step instead of a
`dart pub get` failing in a directory that was never there. `android-setup`
and `ios-setup` pass the same input straight through to their own `codegen`
call.
- **`screenshots`** — `platform`, `driver`/`target`, `locale`, `dart-defines`,
and the **device matrix**: `android-devices` (`"avd:profile:class,…"`) and
`ios-devices` (`"sim|label,…"`). The *simulator/AVD* picks the resolution;
Expand Down
7 changes: 7 additions & 0 deletions actions/android-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ inputs:
default: 'mobile/api'
codegen-tool-dir:
default: 'mobile/tool/openapi_codegen'
generate-openapi:
description: >-
'auto' (default) skips the OpenAPI-client stage when codegen-tool-dir
doesn't exist on disk. 'true'/'false' force the decision either way.
default: 'auto'
sdk-floor:
default: '>=3.12.0 <4.0.0'
clean:
Expand Down Expand Up @@ -89,6 +94,7 @@ runs:
INPUT_PROJECT_DIR: ${{ inputs.project-dir }}
INPUT_API_DIR: ${{ inputs.api-dir }}
INPUT_CODEGEN_TOOL_DIR: ${{ inputs.codegen-tool-dir }}
INPUT_GENERATE_OPENAPI: ${{ inputs.generate-openapi }}
INPUT_SDK_FLOOR: ${{ inputs.sdk-floor }}
INPUT_CLEAN: ${{ inputs.clean }}
INPUT_UPGRADE_DART_STYLE: ${{ inputs.upgrade-dart-style }}
Expand All @@ -98,6 +104,7 @@ runs:
--project-dir "$INPUT_PROJECT_DIR"
--api-dir "$INPUT_API_DIR"
--codegen-tool-dir "$INPUT_CODEGEN_TOOL_DIR"
--generate-openapi "$INPUT_GENERATE_OPENAPI"
--sdk-floor "$INPUT_SDK_FLOOR"
--$([ "$INPUT_CLEAN" = 'true' ] && echo 'clean' || echo 'no-clean')
--$([ "$INPUT_UPGRADE_DART_STYLE" = 'true' ] && echo 'upgrade-dart-style' || echo 'no-upgrade-dart-style')
7 changes: 6 additions & 1 deletion actions/codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ generated `*.g.dart` → app `build_runner` (riverpod / drift / go_router).
| **Runner** | Any — Linux or macOS, GitHub-hosted or self-hosted. |
| **Run before this** | `actions/checkout` (so your code is on disk). That's it — this action sets up Flutter + Java for you (toggle off with `setup-flutter: false` / `setup-java: false` if an earlier step already did). |
| **Secrets** | None. |
| **In your repo** | A layered codegen layout: a Flutter app dir, a generated-client package dir, and a "tool" dir whose `build_runner` drives the OpenAPI Generator. Point the `*-dir` inputs at yours. If you don't use OpenAPI, you probably want the plain `dart run build_runner build` instead of this action. |
| **In your repo** | A layered codegen layout: a Flutter app dir, a generated-client package dir, and a "tool" dir whose `build_runner` drives the OpenAPI Generator. Point the `*-dir` inputs at yours. If you don't use OpenAPI at all, this action still works — `generate-openapi: auto` (the default) detects that `codegen-tool-dir` doesn't exist and runs just the app-level `build_runner` step. |

> **Defaults assume `mobile/`.** If your app lives at the repo root or elsewhere,
> set `project-dir`, `api-dir`, and `codegen-tool-dir` accordingly.
>
> **No OpenAPI client?** Leave `generate-openapi` at its `auto` default (or set
> it to `false` explicitly) — the OpenAPI-client stage is skipped and only the
> app-level `flutter pub get` + `build_runner build` run.

## Usage

Expand All @@ -34,6 +38,7 @@ generated `*.g.dart` → app `build_runner` (riverpod / drift / go_router).
| `project-dir` | no | `mobile` | Your Flutter app directory. |
| `api-dir` | no | `mobile/api` | The generated OpenAPI client package. |
| `codegen-tool-dir` | no | `mobile/tool/openapi_codegen` | Dir whose `build_runner` runs the OpenAPI Generator. |
| `generate-openapi` | no | `auto` | `auto`/`true`/`false`. `auto` skips the OpenAPI-client stage when `codegen-tool-dir` doesn't exist on disk. |
| `clean` | no | `true` | `build_runner clean` + drop `pubspec.lock` first (avoids stale `*.g.dart`). |
| `setup-flutter` / `setup-java` | no | `true` | Let this action install the toolchain. Set `false` if a prior step did. |
| `flutter-version` / `java-version` | no | `3.44.2` / `21` | Versions used when the setups run. |
Expand Down
10 changes: 10 additions & 0 deletions actions/codegen/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ inputs:
codegen-tool-dir:
description: 'Directory whose build_runner drives the OpenAPI Generator CLI.'
default: 'mobile/tool/openapi_codegen'
generate-openapi:
description: >-
'auto' (default) skips the OpenAPI-client stage when codegen-tool-dir
doesn't exist on disk — a consumer with no generated OpenAPI client
just gets the app-level build_runner step, instead of a hard failure
from a command run in a directory that was never there. 'true'/'false'
force the decision either way.
default: 'auto'
api-pubspec-template:
description: >-
Tracked pubspec template copied to <api-dir>/pubspec.yaml before resolve
Expand Down Expand Up @@ -89,6 +97,7 @@ runs:
INPUT_PROJECT_DIR: ${{ inputs.project-dir }}
INPUT_API_DIR: ${{ inputs.api-dir }}
INPUT_CODEGEN_TOOL_DIR: ${{ inputs.codegen-tool-dir }}
INPUT_GENERATE_OPENAPI: ${{ inputs.generate-openapi }}
INPUT_API_PUBSPEC_TEMPLATE: ${{ inputs.api-pubspec-template }}
INPUT_SDK_FLOOR: ${{ inputs.sdk-floor }}
INPUT_CLEAN: ${{ inputs.clean }}
Expand All @@ -99,6 +108,7 @@ runs:
--project-dir "$INPUT_PROJECT_DIR"
--api-dir "$INPUT_API_DIR"
--codegen-tool-dir "$INPUT_CODEGEN_TOOL_DIR"
--generate-openapi "$INPUT_GENERATE_OPENAPI"
--api-pubspec-template "$INPUT_API_PUBSPEC_TEMPLATE"
--sdk-floor "$INPUT_SDK_FLOOR"
--$([ "$INPUT_CLEAN" = 'true' ] && echo 'clean' || echo 'no-clean')
Expand Down
9 changes: 9 additions & 0 deletions actions/ios-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ inputs:
api-pubspec-template:
description: 'Tracked pubspec template copied to <project-dir>/api/pubspec.yaml.'
default: 'mobile/tool/api-pubspec.template.yaml'
generate-openapi:
description: >-
'auto' (default) skips the OpenAPI-client stage when the CLI's default
codegen-tool-dir (mobile/tool/openapi_codegen) doesn't exist on disk.
'true'/'false' force the decision either way. Only matters when
run-codegen is 'true'.
default: 'auto'
clean:
description: 'Drop pubspec.lock + build_runner clean before the API build.'
default: 'true'
Expand Down Expand Up @@ -71,12 +78,14 @@ runs:
INPUT_WORKSPACE: ${{ inputs.workspace }}
INPUT_PROJECT_DIR: ${{ inputs.project-dir }}
INPUT_API_PUBSPEC_TEMPLATE: ${{ inputs.api-pubspec-template }}
INPUT_GENERATE_OPENAPI: ${{ inputs.generate-openapi }}
INPUT_CLEAN: ${{ inputs.clean }}
run: >-
"${{ steps.install.outputs.cli }}" codegen
--workspace "$INPUT_WORKSPACE"
--project-dir "$INPUT_PROJECT_DIR"
--api-pubspec-template "$INPUT_API_PUBSPEC_TEMPLATE"
--generate-openapi "$INPUT_GENERATE_OPENAPI"
--$([ "$INPUT_CLEAN" = 'true' ] && echo 'clean' || echo 'no-clean')

- name: Cache Bundler
Expand Down
42 changes: 40 additions & 2 deletions bin/flutter_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,18 @@ class CodegenCommand extends Command<void> {
defaultsTo: 'mobile/tool/openapi_codegen',
help: 'Dir whose build_runner drives the OpenAPI Generator CLI.',
)
..addOption(
'generate-openapi',
defaultsTo: 'auto',
allowed: ['auto', 'true', 'false'],
help:
'Whether to run the OpenAPI-client stage. "auto" (default) skips '
'it when --codegen-tool-dir does not exist on disk — a consumer '
'with no generated OpenAPI client gets just the app-level '
'build_runner step, instead of a confusing "No such file or '
'directory" from a command run in a directory that was never '
'there. "true"/"false" force the decision either way.',
)
..addOption(
'api-pubspec-template',
defaultsTo: '',
Expand Down Expand Up @@ -352,11 +364,37 @@ class CodegenCommand extends Command<void> {
@override
Future<void> run() async {
final a = argResults!;
final workspace = a.option('workspace')!;
final codegenToolDir = a.option('codegen-tool-dir')!;

final bool generateOpenapi;
switch (a.option('generate-openapi')) {
case 'true':
generateOpenapi = true;
case 'false':
generateOpenapi = false;
stdout.writeln(
'ℹ generate-openapi=false — skipping the OpenAPI-client stage.',
);
default: // 'auto'
final toolDirExists = Directory(
resolveIn(workspace, codegenToolDir),
).existsSync();
generateOpenapi = toolDirExists;
if (!toolDirExists) {
stdout.writeln(
'ℹ generate-openapi=auto — $codegenToolDir does not exist under '
'$workspace, skipping the OpenAPI-client stage.',
);
}
}

final config = CodegenConfig(
workspace: a.option('workspace')!,
workspace: workspace,
projectDir: a.option('project-dir')!,
apiDir: a.option('api-dir')!,
codegenToolDir: a.option('codegen-tool-dir')!,
codegenToolDir: codegenToolDir,
generateOpenapi: generateOpenapi,
apiPubspecTemplate: a.option('api-pubspec-template')!,
sdkFloor: a.option('sdk-floor')!,
clean: a.flag('clean'),
Expand Down
2 changes: 1 addition & 1 deletion cli-version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.2.0
125 changes: 73 additions & 52 deletions lib/src/codegen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CodegenConfig {
this.projectDir = 'mobile',
this.apiDir = 'mobile/api',
this.codegenToolDir = 'mobile/tool/openapi_codegen',
this.generateOpenapi = true,
this.apiPubspecTemplate = '',
this.sdkFloor = '>=3.12.0 <4.0.0',
this.clean = true,
Expand All @@ -30,6 +31,20 @@ class CodegenConfig {
/// Sub-project that drives the OpenAPI Generator CLI (writes [apiDir]/*.dart).
final String codegenToolDir;

/// Whether to run the OpenAPI-client stage (steps 1-3 below) at all.
///
/// A consumer with no generated OpenAPI client (e.g. one that deleted
/// [codegenToolDir]/[apiDir] entirely) has no use for this stage, and the
/// underlying commands hard-fail with a confusing "No such file or
/// directory" if pointed at a directory that doesn't exist. The `codegen`
/// action's own CLI layer resolves this from its `generate-openapi`
/// input — `auto` (the default) checks whether [codegenToolDir] exists on
/// disk and sets this accordingly; `true`/`false` force it either way. This
/// field is always an explicit bool by the time it reaches [planCodegen] —
/// the planner itself stays pure (no I/O) so it's testable without
/// touching a filesystem.
final bool generateOpenapi;

/// Optional tracked template copied to `<apiDir>/pubspec.yaml` before resolve.
/// When empty, [sdkFloor] is patched into the existing pubspec instead.
final String apiPubspecTemplate;
Expand Down Expand Up @@ -58,75 +73,81 @@ class CodegenConfig {
/// 2. materialise/patch the API `pubspec.yaml` SDK floor
/// 3. API package `build_runner` → `<apiDir>/*.g.dart`
/// 4. app `flutter pub get` + `build_runner` → riverpod/drift/go_router code
///
/// Steps 1-3 (the OpenAPI-client stage) are entirely omitted when
/// [CodegenConfig.generateOpenapi] is false — a consumer with no generated
/// OpenAPI client just gets step 4.
List<Step> planCodegen(CodegenConfig c) {
final toolDir = resolveIn(c.workspace, c.codegenToolDir);
final apiDir = resolveIn(c.workspace, c.apiDir);
final projectDir = resolveIn(c.workspace, c.projectDir);
final apiPubspec = resolveIn(apiDir, 'pubspec.yaml');

final steps = <Step>[
// 1. Run the OpenAPI Generator CLI via its build_runner driver.
RunStep(
label: 'Generate OpenAPI client (Java CLI)',
executable: c.dart,
args: const ['pub', 'get'],
workingDir: toolDir,
),
RunStep(
label: 'OpenAPI Generator build_runner',
executable: c.dart,
args: ['run', 'build_runner', 'build', ...c.buildRunnerArgs],
workingDir: toolDir,
),

// 2. Make sure the generated API pubspec has a usable SDK floor. A tracked
// template wins (also restores the file on runners where it's gitignored);
// otherwise patch the floor in place.
if (c.apiPubspecTemplate.isNotEmpty)
CopyFileStep(
label: 'Materialise API pubspec from template',
from: resolveIn(c.workspace, c.apiPubspecTemplate),
to: apiPubspec,
)
else
PatchSdkFloorStep(
label: 'Patch API pubspec SDK floor',
path: apiPubspec,
sdkFloor: c.sdkFloor,
if (c.generateOpenapi) ...[
// 1. Run the OpenAPI Generator CLI via its build_runner driver.
RunStep(
label: 'Generate OpenAPI client (Java CLI)',
executable: c.dart,
args: const ['pub', 'get'],
workingDir: toolDir,
),

// 3. Generate the API package's *.g.dart (json_serializable / copy_with).
if (c.clean)
DeleteFileStep(
label: 'Drop stale API pubspec.lock',
path: resolveIn(apiDir, 'pubspec.lock'),
RunStep(
label: 'OpenAPI Generator build_runner',
executable: c.dart,
args: ['run', 'build_runner', 'build', ...c.buildRunnerArgs],
workingDir: toolDir,
),
RunStep(
label: 'API pub get',
executable: c.dart,
args: const ['pub', 'get'],
workingDir: apiDir,
),
if (c.upgradeDartStyle)

// 2. Make sure the generated API pubspec has a usable SDK floor. A
// tracked template wins (also restores the file on runners where
// it's gitignored); otherwise patch the floor in place.
if (c.apiPubspecTemplate.isNotEmpty)
CopyFileStep(
label: 'Materialise API pubspec from template',
from: resolveIn(c.workspace, c.apiPubspecTemplate),
to: apiPubspec,
)
else
PatchSdkFloorStep(
label: 'Patch API pubspec SDK floor',
path: apiPubspec,
sdkFloor: c.sdkFloor,
),

// 3. Generate the API package's *.g.dart (json_serializable / copy_with).
if (c.clean)
DeleteFileStep(
label: 'Drop stale API pubspec.lock',
path: resolveIn(apiDir, 'pubspec.lock'),
),
RunStep(
label: 'Upgrade dart_style',
label: 'API pub get',
executable: c.dart,
args: const ['pub', 'upgrade', 'dart_style'],
args: const ['pub', 'get'],
workingDir: apiDir,
),
if (c.clean)
if (c.upgradeDartStyle)
RunStep(
label: 'Upgrade dart_style',
executable: c.dart,
args: const ['pub', 'upgrade', 'dart_style'],
workingDir: apiDir,
),
if (c.clean)
RunStep(
label: 'API build_runner clean',
executable: c.dart,
args: const ['run', 'build_runner', 'clean'],
workingDir: apiDir,
),
RunStep(
label: 'API build_runner clean',
label: 'API build_runner build',
executable: c.dart,
args: const ['run', 'build_runner', 'clean'],
args: ['run', 'build_runner', 'build', ...c.buildRunnerArgs],
workingDir: apiDir,
),
RunStep(
label: 'API build_runner build',
executable: c.dart,
args: ['run', 'build_runner', 'build', ...c.buildRunnerArgs],
workingDir: apiDir,
),
],

// 4. App-level codegen (riverpod / drift / go_router).
RunStep(
Expand Down
39 changes: 39 additions & 0 deletions test/codegen_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,45 @@ void main() {
);
});

test('generateOpenapi=false skips the OpenAPI-client stage entirely, '
'keeping only the app-level build', () {
final steps = planCodegen(
const CodegenConfig(workspace: '/w', generateOpenapi: false),
);
final labels = steps.map((s) => s.label).toList();

expect(labels, ['App flutter pub get', 'App build_runner build']);
expect(steps.whereType<PatchSdkFloorStep>(), isEmpty);
expect(steps.whereType<CopyFileStep>(), isEmpty);
expect(steps.whereType<DeleteFileStep>(), isEmpty);
});

test(
'generateOpenapi=false ignores clean/upgradeDartStyle/apiPubspecTemplate '
'(nothing left to apply them to)',
() {
final steps = planCodegen(
const CodegenConfig(
workspace: '/w',
generateOpenapi: false,
clean: true,
upgradeDartStyle: true,
apiPubspecTemplate: 'mobile/openapi/api-pubspec.yaml',
),
);
final labels = steps.map((s) => s.label).toList();
expect(labels, ['App flutter pub get', 'App build_runner build']);
},
);

test('generateOpenapi defaults to true (unchanged default behaviour)', () {
final steps = planCodegen(const CodegenConfig(workspace: '/w'));
expect(
steps.map((s) => s.label),
contains('Generate OpenAPI client (Java CLI)'),
);
});

test('custom build-runner args propagate to every build_runner build', () {
final steps = planCodegen(
const CodegenConfig(
Expand Down