diff --git a/README.md b/README.md index 0b66c54..d49febe 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/actions/android-setup/action.yml b/actions/android-setup/action.yml index 7e99b1f..b005b94 100644 --- a/actions/android-setup/action.yml +++ b/actions/android-setup/action.yml @@ -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: @@ -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 }} @@ -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') \ No newline at end of file diff --git a/actions/codegen/README.md b/actions/codegen/README.md index d9ad7f4..d647be9 100644 --- a/actions/codegen/README.md +++ b/actions/codegen/README.md @@ -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 @@ -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. | diff --git a/actions/codegen/action.yml b/actions/codegen/action.yml index 84d5f6a..f6d6330 100644 --- a/actions/codegen/action.yml +++ b/actions/codegen/action.yml @@ -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 /pubspec.yaml before resolve @@ -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 }} @@ -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') diff --git a/actions/ios-setup/action.yml b/actions/ios-setup/action.yml index 2e2c12f..c79909c 100644 --- a/actions/ios-setup/action.yml +++ b/actions/ios-setup/action.yml @@ -13,6 +13,13 @@ inputs: api-pubspec-template: description: 'Tracked pubspec template copied to /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' @@ -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 diff --git a/bin/flutter_tools.dart b/bin/flutter_tools.dart index 27506d6..e46d2fb 100644 --- a/bin/flutter_tools.dart +++ b/bin/flutter_tools.dart @@ -307,6 +307,18 @@ class CodegenCommand extends Command { 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: '', @@ -352,11 +364,37 @@ class CodegenCommand extends Command { @override Future 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'), diff --git a/cli-version.txt b/cli-version.txt index 6e8bf73..0ea3a94 100644 --- a/cli-version.txt +++ b/cli-version.txt @@ -1 +1 @@ -0.1.0 +0.2.0 diff --git a/lib/src/codegen.dart b/lib/src/codegen.dart index be2ad4e..47dcdf8 100644 --- a/lib/src/codegen.dart +++ b/lib/src/codegen.dart @@ -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, @@ -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 `/pubspec.yaml` before resolve. /// When empty, [sdkFloor] is patched into the existing pubspec instead. final String apiPubspecTemplate; @@ -58,6 +73,10 @@ class CodegenConfig { /// 2. materialise/patch the API `pubspec.yaml` SDK floor /// 3. API package `build_runner` → `/*.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 planCodegen(CodegenConfig c) { final toolDir = resolveIn(c.workspace, c.codegenToolDir); final apiDir = resolveIn(c.workspace, c.apiDir); @@ -65,68 +84,70 @@ List planCodegen(CodegenConfig c) { final apiPubspec = resolveIn(apiDir, 'pubspec.yaml'); final steps = [ - // 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( diff --git a/test/codegen_test.dart b/test/codegen_test.dart index da101aa..9a8bd81 100644 --- a/test/codegen_test.dart +++ b/test/codegen_test.dart @@ -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(), isEmpty); + expect(steps.whereType(), isEmpty); + expect(steps.whereType(), 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(