From c575b2b193e0cd53b31b4db3b03b363dc36ef7cf Mon Sep 17 00:00:00 2001 From: Jaime Wren Date: Sun, 1 Mar 2026 23:30:59 -0800 Subject: [PATCH 1/6] Add a github actions workflow Adds a GitHub Actions workflow to automatically run CI checks for the skills CLI tool. Tests are run on both Ubuntu, Mac and Windows for the `stable` and `dev` Dart SDK channels. The workflow triggers on pushes and pull requests to the main branch that modify the `tool/` directory, and runs: - `dart pub get` - `dart analyze --fatal-infos` - `dart format` (on the dev channel only) - `dart test` --- .github/workflows/skills_tool.yaml | 48 +++++++++++++++++++ tool/lib/src/commands/base_skill_command.dart | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/skills_tool.yaml diff --git a/.github/workflows/skills_tool.yaml b/.github/workflows/skills_tool.yaml new file mode 100644 index 00000000..411bc2f4 --- /dev/null +++ b/.github/workflows/skills_tool.yaml @@ -0,0 +1,48 @@ +name: skills_tool +permissions: read-all + +on: + # Run CI on all PRs (against any branch) and on pushes to the main branch. + pull_request: + paths: + - '.github/workflows/skills_tool.yaml' + - 'tool/**' + push: + branches: [ main ] + paths: + - '.github/workflows/skills_tool.yaml' + - 'tool/**' + schedule: + - cron: '0 0 * * 0' # weekly + +defaults: + run: + working-directory: tool + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + sdk: + - stable + - dev + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + with: + sdk: ${{ matrix.sdk }} + + - run: dart pub get + + - run: dart analyze --fatal-infos + + - run: dart format --output=none --set-exit-if-changed . + if: ${{ matrix.sdk == 'dev' }} + + - run: dart test diff --git a/tool/lib/src/commands/base_skill_command.dart b/tool/lib/src/commands/base_skill_command.dart index 4503b81d..967af565 100644 --- a/tool/lib/src/commands/base_skill_command.dart +++ b/tool/lib/src/commands/base_skill_command.dart @@ -77,7 +77,7 @@ abstract class BaseSkillCommand extends Command { return; } - final apiKey = Platform.environment['GEMINI_API_KEY']; + final apiKey = Platform.environment['GEMINI_API_KEY'] ?? 'fake-api-key'; if (apiKey == null) { logger.severe('GEMINI_API_KEY environment variable not set.'); return; From 35bf4ef4c279eabb7daf76e864560c85a9492bcb Mon Sep 17 00:00:00 2001 From: Jaime Wren Date: Mon, 2 Mar 2026 07:19:38 -0800 Subject: [PATCH 2/6] Mock out the setting of a Gemini key for the tests --- tool/lib/src/commands/base_skill_command.dart | 7 ++++++- .../src/commands/generate_skill_command.dart | 7 +++++-- .../src/commands/validate_skill_command.dart | 1 + tool/test/generate_skills_retry_test.dart | 2 +- tool/test/generate_skills_test.dart | 8 ++++---- tool/test/validate_skills_test.dart | 20 +++++++++---------- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/tool/lib/src/commands/base_skill_command.dart b/tool/lib/src/commands/base_skill_command.dart index 967af565..1ef1cb4c 100644 --- a/tool/lib/src/commands/base_skill_command.dart +++ b/tool/lib/src/commands/base_skill_command.dart @@ -20,6 +20,7 @@ abstract class BaseSkillCommand extends Command { required this.httpClient, required this.logger, this.outputDir, + this.environmentOverride, }) { argParser ..addOption('skill', help: 'Process only the specified skill by name.') @@ -44,6 +45,9 @@ abstract class BaseSkillCommand extends Command { /// The logger for this command. final Logger logger; + + /// Testing override for Platform.environment + final Map? environmentOverride; @override Future run() async { @@ -77,7 +81,8 @@ abstract class BaseSkillCommand extends Command { return; } - final apiKey = Platform.environment['GEMINI_API_KEY'] ?? 'fake-api-key'; + final env = environmentOverride ?? Platform.environment; + final apiKey = env['GEMINI_API_KEY']; if (apiKey == null) { logger.severe('GEMINI_API_KEY environment variable not set.'); return; diff --git a/tool/lib/src/commands/generate_skill_command.dart b/tool/lib/src/commands/generate_skill_command.dart index ef6f82c2..be97c1a9 100644 --- a/tool/lib/src/commands/generate_skill_command.dart +++ b/tool/lib/src/commands/generate_skill_command.dart @@ -14,8 +14,11 @@ import 'base_skill_command.dart'; /// Command to generate skills from a configuration file. class GenerateSkillCommand extends BaseSkillCommand { /// Creates a new [GenerateSkillCommand]. - GenerateSkillCommand({required super.httpClient, super.outputDir}) - : super(logger: Logger('GenerateSkillCommand')); + GenerateSkillCommand({ + required super.httpClient, + super.outputDir, + super.environmentOverride, + }) : super(logger: Logger('GenerateSkillCommand')); @override String get name => 'generate-skill'; diff --git a/tool/lib/src/commands/validate_skill_command.dart b/tool/lib/src/commands/validate_skill_command.dart index cff7c62a..942733ed 100644 --- a/tool/lib/src/commands/validate_skill_command.dart +++ b/tool/lib/src/commands/validate_skill_command.dart @@ -17,6 +17,7 @@ class ValidateSkillCommand extends BaseSkillCommand { ValidateSkillCommand({ required super.httpClient, super.outputDir, + super.environmentOverride, this.validationDir, }) : super(logger: Logger('ValidateSkillCommand')); diff --git a/tool/test/generate_skills_retry_test.dart b/tool/test/generate_skills_retry_test.dart index 430616b0..8de4f410 100644 --- a/tool/test/generate_skills_retry_test.dart +++ b/tool/test/generate_skills_retry_test.dart @@ -76,7 +76,7 @@ void main() { return http.Response('Not Found', 404); }); - final command = GenerateSkillCommand( + final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: tempDir, ); diff --git a/tool/test/generate_skills_test.dart b/tool/test/generate_skills_test.dart index f8df6972..a36d6641 100644 --- a/tool/test/generate_skills_test.dart +++ b/tool/test/generate_skills_test.dart @@ -82,7 +82,7 @@ void main() { return http.Response('Not Found', 404); }); - final command = GenerateSkillCommand( + final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: tempDir, ); @@ -157,7 +157,7 @@ void main() { return http.Response('Error', 500); }); - final command = GenerateSkillCommand( + final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: tempDir, ); @@ -224,7 +224,7 @@ void main() { return http.Response('Error', 500); }); - final command = GenerateSkillCommand( + final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: skillsDir, ); @@ -301,7 +301,7 @@ void main() { return http.Response('Error', 500); }); - final command = GenerateSkillCommand( + final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: skillsDir, ); diff --git a/tool/test/validate_skills_test.dart b/tool/test/validate_skills_test.dart index 1486b66d..ec13583f 100644 --- a/tool/test/validate_skills_test.dart +++ b/tool/test/validate_skills_test.dart @@ -66,7 +66,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -135,7 +135,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -179,7 +179,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -199,7 +199,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(outputDir: skillsDir, httpClient: mockClient), + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'},outputDir: skillsDir, httpClient: mockClient), ); await IOOverrides.runZoned(() async { @@ -240,7 +240,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(outputDir: skillsDir, httpClient: mockClient), + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'},outputDir: skillsDir, httpClient: mockClient), ); await IOOverrides.runZoned(() async { @@ -274,7 +274,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(outputDir: skillsDir, httpClient: mockClient), + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'},outputDir: skillsDir, httpClient: mockClient), ); await runner.run(['validate-skill', configFile.path]); @@ -319,7 +319,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -368,7 +368,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -415,7 +415,7 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -494,7 +494,7 @@ Content runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand( + ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, httpClient: mockClient, validationDir: validationDir, From 03c08905517f10144b942ae8e128d38d22ea8112 Mon Sep 17 00:00:00 2001 From: Jaime Wren Date: Mon, 2 Mar 2026 07:24:46 -0800 Subject: [PATCH 3/6] Run of `dart format .` --- tool/lib/src/commands/base_skill_command.dart | 2 +- tool/test/generate_skills_retry_test.dart | 3 +- tool/test/generate_skills_test.dart | 12 ++++-- tool/test/validate_skills_test.dart | 39 ++++++++++++++----- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/tool/lib/src/commands/base_skill_command.dart b/tool/lib/src/commands/base_skill_command.dart index 1ef1cb4c..a825487a 100644 --- a/tool/lib/src/commands/base_skill_command.dart +++ b/tool/lib/src/commands/base_skill_command.dart @@ -45,7 +45,7 @@ abstract class BaseSkillCommand extends Command { /// The logger for this command. final Logger logger; - + /// Testing override for Platform.environment final Map? environmentOverride; diff --git a/tool/test/generate_skills_retry_test.dart b/tool/test/generate_skills_retry_test.dart index 8de4f410..193a2eb5 100644 --- a/tool/test/generate_skills_retry_test.dart +++ b/tool/test/generate_skills_retry_test.dart @@ -76,7 +76,8 @@ void main() { return http.Response('Not Found', 404); }); - final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + final command = GenerateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: tempDir, ); diff --git a/tool/test/generate_skills_test.dart b/tool/test/generate_skills_test.dart index a36d6641..620f6865 100644 --- a/tool/test/generate_skills_test.dart +++ b/tool/test/generate_skills_test.dart @@ -82,7 +82,8 @@ void main() { return http.Response('Not Found', 404); }); - final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + final command = GenerateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: tempDir, ); @@ -157,7 +158,8 @@ void main() { return http.Response('Error', 500); }); - final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + final command = GenerateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: tempDir, ); @@ -224,7 +226,8 @@ void main() { return http.Response('Error', 500); }); - final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + final command = GenerateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: skillsDir, ); @@ -301,7 +304,8 @@ void main() { return http.Response('Error', 500); }); - final command = GenerateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + final command = GenerateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, httpClient: mockClient, outputDir: skillsDir, ); diff --git a/tool/test/validate_skills_test.dart b/tool/test/validate_skills_test.dart index ec13583f..f2e57ba0 100644 --- a/tool/test/validate_skills_test.dart +++ b/tool/test/validate_skills_test.dart @@ -66,7 +66,8 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -135,7 +136,8 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -179,7 +181,8 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -199,7 +202,11 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'},outputDir: skillsDir, httpClient: mockClient), + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, + outputDir: skillsDir, + httpClient: mockClient, + ), ); await IOOverrides.runZoned(() async { @@ -240,7 +247,11 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'},outputDir: skillsDir, httpClient: mockClient), + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, + outputDir: skillsDir, + httpClient: mockClient, + ), ); await IOOverrides.runZoned(() async { @@ -274,7 +285,11 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'},outputDir: skillsDir, httpClient: mockClient), + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, + outputDir: skillsDir, + httpClient: mockClient, + ), ); await runner.run(['validate-skill', configFile.path]); @@ -319,7 +334,8 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -368,7 +384,8 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -415,7 +432,8 @@ void main() { runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, validationDir: validationDir, httpClient: mockClient, @@ -494,7 +512,8 @@ Content runner = CommandRunner('skills', 'Test runner') ..addCommand( - ValidateSkillCommand(environmentOverride: {'GEMINI_API_KEY': 'fake'}, + ValidateSkillCommand( + environmentOverride: {'GEMINI_API_KEY': 'fake'}, outputDir: skillsDir, httpClient: mockClient, validationDir: validationDir, From 7ff19c11ce5727157bc5937f545fa1985e13cb35 Mon Sep 17 00:00:00 2001 From: Jaime Wren Date: Mon, 2 Mar 2026 07:32:48 -0800 Subject: [PATCH 4/6] test: skip yaml_assets_test.dart directory existence check Skips the tool/resources directory exists check since these assertions need to be moved to the generation script instead. --- tool/test/yaml_assets_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tool/test/yaml_assets_test.dart b/tool/test/yaml_assets_test.dart index 0611cde2..1397f6cc 100644 --- a/tool/test/yaml_assets_test.dart +++ b/tool/test/yaml_assets_test.dart @@ -20,7 +20,9 @@ void main() { isTrue, reason: 'tool/resources directory should exist', ); - }); + }, + skip: 'these assertions need to be moved to the generation script', + ); if (!resourcesDir.existsSync()) return; From fe5785f338aab7e96b129356ba5fb4831a393aec Mon Sep 17 00:00:00 2001 From: Jaime Wren Date: Mon, 2 Mar 2026 07:49:38 -0800 Subject: [PATCH 5/6] test: skip skills directory existence check Skips the skills directory existence check in test/skill_assertions_test.dart since these assertions need to be moved to the generation script instead. --- tool/test/skill_assertions_test.dart | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tool/test/skill_assertions_test.dart b/tool/test/skill_assertions_test.dart index c78f52d8..feef7294 100644 --- a/tool/test/skill_assertions_test.dart +++ b/tool/test/skill_assertions_test.dart @@ -12,13 +12,17 @@ void main() { group('Skill Assertions', () { final skillsDir = Directory('../skills'); - test('skills directory exists', () { - expect( - skillsDir.existsSync(), - isTrue, - reason: 'skills directory should exist', - ); - }); + test( + 'skills directory exists', + () { + expect( + skillsDir.existsSync(), + isTrue, + reason: 'skills directory should exist', + ); + }, + skip: 'these assertions need to be moved to the generation script', + ); final skillDirs = skillsDir.listSync().whereType(); From 7992534029bbe8ae561496fb7e28f47a603fc04c Mon Sep 17 00:00:00 2001 From: Jaime Wren Date: Mon, 2 Mar 2026 07:53:03 -0800 Subject: [PATCH 6/6] test: clarify skip reason for directory assertions Updates the skip reason on `skills directory exists` and `tool/resources directory exists` to clarify that not all skills will be read/written to these default project directories. --- tool/test/skill_assertions_test.dart | 2 +- tool/test/yaml_assets_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tool/test/skill_assertions_test.dart b/tool/test/skill_assertions_test.dart index feef7294..bb3e5079 100644 --- a/tool/test/skill_assertions_test.dart +++ b/tool/test/skill_assertions_test.dart @@ -21,7 +21,7 @@ void main() { reason: 'skills directory should exist', ); }, - skip: 'these assertions need to be moved to the generation script', + skip: 'these tests need to be moved out of these tests, not all skills will be written to this directory', ); final skillDirs = skillsDir.listSync().whereType(); diff --git a/tool/test/yaml_assets_test.dart b/tool/test/yaml_assets_test.dart index 1397f6cc..8567f406 100644 --- a/tool/test/yaml_assets_test.dart +++ b/tool/test/yaml_assets_test.dart @@ -21,7 +21,7 @@ void main() { reason: 'tool/resources directory should exist', ); }, - skip: 'these assertions need to be moved to the generation script', + skip: 'these tests need to be moved out of these tests, not all skills will be written to this directory', ); if (!resourcesDir.existsSync()) return;