diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed70e1c..498ce32 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: jobs: - ckeck: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,9 +23,8 @@ jobs: - name: Analyze project source run: dart analyze - # No tests to run yet - # - name: Run tests - # run: dart test + - name: Run tests + run: dart test build-linux: runs-on: ubuntu-latest diff --git a/README.md b/README.md index e3ff276..1f3ffdc 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,22 @@ Or use directly from sources: dart bin/raygun_cli.dart ``` -**Common mandatory arguments** +#### Configuration parameters -- `app-id` the Application ID in Raygun.com. -- `token` is an access token from https://app.raygun.com/user/tokens. +All `raygun-cli` commands share the same configuration parameters. + +- App ID: The Application ID in Raygun.com. +- Token: An access token from https://app.raygun.com/user/tokens. + +You can pass these parameters via arguments, e.g. `--app-id=` +or you can set them as environment variables. + +Parameters passed as arguments have priority over environment variables. + +| Parameter | Argument | Environment Variable | +|-----------|----------|----------------------| +| App ID | `app-id` | `RAYGUN_APP_ID` | +| Token | `token` | `RAYGUN_TOKEN` | #### Sourcemap Uploader diff --git a/bin/raygun_cli.dart b/bin/raygun_cli.dart index 751cf50..74c6e7e 100644 --- a/bin/raygun_cli.dart +++ b/bin/raygun_cli.dart @@ -2,7 +2,7 @@ import 'package:args/args.dart'; import 'package:raygun_cli/sourcemap/sourcemap_command.dart'; import 'package:raygun_cli/symbols/flutter_symbols.dart'; -const String version = '0.0.1'; +const String version = '0.0.2'; ArgParser buildParser() { return ArgParser() @@ -34,8 +34,16 @@ ArgParser buildParser() { } void printUsage(ArgParser argParser) { - print('Usage: raygun-cli [arguments]'); + print('Raygun CLI: $version'); + print(''); + print('Usage: raygun-cli '); print(argParser.usage); + print(''); + print('Commands:'); + for (final command in argParser.commands.keys) { + print(' $command'); + } + print(''); } void main(List arguments) { diff --git a/lib/config_props.dart b/lib/config_props.dart new file mode 100644 index 0000000..b27202d --- /dev/null +++ b/lib/config_props.dart @@ -0,0 +1,62 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:raygun_cli/environment.dart'; + +/// Configuration properties for the Raygun CLI +class ConfigProps { + /// Raygun's application ID + final String appId; + + /// Raygun's access token + final String token; + + ConfigProps._({ + required this.appId, + required this.token, + }); + + /// Load configuration properties from arguments or environment variables + /// and return a new instance of [ConfigProps] or exit with code 2. + factory ConfigProps.load(ArgResults arguments, {bool verbose = false}) { + String? appId; + String? token; + + // Providing app-id and token via argument takes priority + if (arguments.wasParsed('app-id')) { + appId = arguments['app-id']; + } else { + appId = Environment.instance.raygunAppId; + } + + if (appId == null) { + print('Error: Missing "app-id"'); + print( + ' Please provide "app-id" via argument or environment variable "RAYGUN_APP_ID"'); + exit(2); + } + + if (arguments.wasParsed('token')) { + token = arguments['token']; + } else { + token = Environment.instance.raygunToken; + } + + if (token == null) { + print('Error: Missing "token"'); + print( + ' Please provide "token" via argument or environment variable "RAYGUN_TOKEN"'); + exit(2); + } + + if (verbose) { + print('App ID: $appId'); + print('Token: $token'); + } + + return ConfigProps._( + appId: appId, + token: token, + ); + } +} diff --git a/lib/environment.dart b/lib/environment.dart new file mode 100644 index 0000000..2fda981 --- /dev/null +++ b/lib/environment.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +/// Wraps access to Environment variables +/// Allows faking for testing +class Environment { + static String raygunAppIdKey = 'RAYGUN_APP_ID'; + static String raygunTokenKey = 'RAYGUN_TOKEN'; + + final String? raygunAppId; + final String? raygunToken; + + static Environment? _instance; + + /// Singleton instance access + /// Will init if not already + static Environment get instance { + _instance ??= Environment._init(); + return _instance!; + } + + /// For testing purposes + static void setInstance(Environment instance) { + _instance = instance; + } + + /// Create custom instance + Environment({ + required this.raygunAppId, + required this.raygunToken, + }); + + factory Environment._init() { + final raygunAppId = Platform.environment[raygunAppIdKey]; + final raygunToken = Platform.environment[raygunTokenKey]; + return Environment( + raygunAppId: raygunAppId, + raygunToken: raygunToken, + ); + } +} diff --git a/lib/sourcemap/flutter/sourcemap_flutter.dart b/lib/sourcemap/flutter/sourcemap_flutter.dart index e095a25..18b5395 100644 --- a/lib/sourcemap/flutter/sourcemap_flutter.dart +++ b/lib/sourcemap/flutter/sourcemap_flutter.dart @@ -7,6 +7,7 @@ class SourcemapFlutter extends SourcemapBase { SourcemapFlutter({ required super.command, required super.verbose, + required super.config, }); @override diff --git a/lib/sourcemap/node/sourcemap_node.dart b/lib/sourcemap/node/sourcemap_node.dart index 5ebb10a..6c67478 100644 --- a/lib/sourcemap/node/sourcemap_node.dart +++ b/lib/sourcemap/node/sourcemap_node.dart @@ -6,6 +6,7 @@ class SourcemapNode extends SourcemapBase { SourcemapNode({ required super.command, required super.verbose, + required super.config, }); @override diff --git a/lib/sourcemap/sourcemap_base.dart b/lib/sourcemap/sourcemap_base.dart index 999f176..ab1c2fa 100644 --- a/lib/sourcemap/sourcemap_base.dart +++ b/lib/sourcemap/sourcemap_base.dart @@ -1,12 +1,15 @@ import 'package:args/args.dart'; +import '../config_props.dart'; + abstract class SourcemapBase { SourcemapBase({ required this.command, required this.verbose, + required ConfigProps config, }) { - appId = command.option('app-id')!; - token = command.option('token')!; + appId = config.appId; + token = config.token; } final ArgResults command; diff --git a/lib/sourcemap/sourcemap_command.dart b/lib/sourcemap/sourcemap_command.dart index 98d8a13..3e60f74 100644 --- a/lib/sourcemap/sourcemap_command.dart +++ b/lib/sourcemap/sourcemap_command.dart @@ -5,6 +5,8 @@ import 'package:raygun_cli/sourcemap/flutter/sourcemap_flutter.dart'; import 'package:raygun_cli/sourcemap/node/sourcemap_node.dart'; import 'package:raygun_cli/sourcemap/sourcemap_single_file.dart'; +import '../config_props.dart'; + const kSourcemapCommand = 'sourcemap'; ArgParser buildParserSourcemap() { @@ -18,12 +20,10 @@ ArgParser buildParserSourcemap() { ..addOption( 'app-id', help: 'Raygun\'s application ID', - mandatory: true, ) ..addOption( 'token', help: 'Raygun\'s access token', - mandatory: true, ) ..addOption( 'platform', @@ -56,18 +56,27 @@ void parseSourcemapCommand(ArgResults command, bool verbose) { print(buildParserSourcemap().usage); exit(0); } - if (!command.wasParsed('app-id') || !command.wasParsed('token')) { - print('Missing mandatory arguments'); - print(buildParserSourcemap().usage); - exit(2); - } + final configProps = ConfigProps.load(command, verbose: verbose); + switch (command.option('platform')) { case null: - SourcemapSingleFile(command: command, verbose: verbose).upload(); + SourcemapSingleFile( + command: command, + verbose: verbose, + config: configProps, + ).upload(); case 'flutter': - SourcemapFlutter(command: command, verbose: verbose).upload(); + SourcemapFlutter( + command: command, + verbose: verbose, + config: configProps, + ).upload(); case 'node': - SourcemapNode(command: command, verbose: verbose).upload(); + SourcemapNode( + command: command, + verbose: verbose, + config: configProps, + ).upload(); default: print('Unsupported platform'); exit(1); diff --git a/lib/sourcemap/sourcemap_single_file.dart b/lib/sourcemap/sourcemap_single_file.dart index a478b10..9a65f6f 100644 --- a/lib/sourcemap/sourcemap_single_file.dart +++ b/lib/sourcemap/sourcemap_single_file.dart @@ -7,6 +7,7 @@ class SourcemapSingleFile extends SourcemapBase { SourcemapSingleFile({ required super.command, required super.verbose, + required super.config, }); @override diff --git a/lib/symbols/flutter_symbols.dart b/lib/symbols/flutter_symbols.dart index 2bc6292..209189e 100644 --- a/lib/symbols/flutter_symbols.dart +++ b/lib/symbols/flutter_symbols.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:args/args.dart'; +import 'package:raygun_cli/config_props.dart'; import 'package:raygun_cli/symbols/flutter_symbols_api.dart'; const kSymbolsCommand = 'symbols'; @@ -12,15 +13,11 @@ void parseSymbolsCommand(ArgResults command, bool verbose) { print(buildParserSymbols().usage); exit(0); } - if (!command.wasParsed('app-id') || !command.wasParsed('token')) { - print('Missing mandatory arguments'); - print(buildParserSymbols().usage); - exit(2); - } + final configProps = ConfigProps.load(command, verbose: verbose); _run( command: command, - appId: command['app-id'], - token: command['token'], + appId: configProps.appId, + token: configProps.token, ).then((result) { if (result) { exit(0); @@ -88,12 +85,10 @@ ArgParser buildParserSymbols() { ..addOption( 'app-id', help: 'Raygun\'s application ID', - mandatory: true, ) ..addOption( 'token', help: 'Raygun\'s access token', - mandatory: true, ) ..addOption( 'path', diff --git a/pubspec.lock b/pubspec.lock index 3abb0c6..b362e98 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -407,4 +407,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index e07efb3..9e2c032 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,6 @@ name: raygun_cli description: Command-line tool for Raygun.com +# Update version in bin/raygun_cli.dart as well version: 0.0.2 repository: https://github.com/MindscapeHQ/raygun-cli/ homepage: https://raygun.com diff --git a/test/.gitkeep b/test/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/config_props_test.dart b/test/config_props_test.dart new file mode 100644 index 0000000..51b8af7 --- /dev/null +++ b/test/config_props_test.dart @@ -0,0 +1,94 @@ +import 'package:args/args.dart'; +import 'package:raygun_cli/config_props.dart'; +import 'package:raygun_cli/environment.dart'; +import 'package:test/test.dart'; + +void main() { + group('ConfigProps', () { + test('should parse arguments', () { + ArgParser parser = ArgParser() + ..addFlag('verbose') + ..addOption('app-id') + ..addOption('token'); + final results = + parser.parse(['--app-id=app-id-parsed', '--token=token-parsed']); + final props = ConfigProps.load(results); + expect(props.appId, 'app-id-parsed'); + expect(props.token, 'token-parsed'); + }); + + test('should parse from env vars', () { + // fake environment variables + Environment.setInstance( + Environment( + raygunAppId: 'app-id-env', + raygunToken: 'token-env', + ), + ); + + // define parser + ArgParser parser = ArgParser() + ..addFlag('verbose') + ..addOption('app-id') + ..addOption('token'); + + // parse nothing + final results = parser.parse([]); + + // load from env vars + final props = ConfigProps.load(results); + expect(props.appId, 'app-id-env'); + expect(props.token, 'token-env'); + }); + + test('should parse with priority', () { + // fake environment variables + Environment.setInstance( + Environment( + raygunAppId: 'app-id-env', + raygunToken: 'token-env', + ), + ); + + // define parser + ArgParser parser = ArgParser() + ..addFlag('verbose') + ..addOption('app-id') + ..addOption('token'); + + // parse arguments + final results = + parser.parse(['--app-id=app-id-parsed', '--token=token-parsed']); + + // load from parsed even if env vars are set + final props = ConfigProps.load(results); + expect(props.appId, 'app-id-parsed'); + expect(props.token, 'token-parsed'); + }); + + test('should parse from both', () { + // fake environment variables + // token is not provided + Environment.setInstance( + Environment( + raygunAppId: 'app-id-env', + raygunToken: null, + ), + ); + + // define parser + ArgParser parser = ArgParser() + ..addFlag('verbose') + ..addOption('app-id') + ..addOption('token'); + + // parse arguments, only token is passed + final results = parser.parse(['--token=token-parsed']); + + // app-id from env, token from argument + final props = ConfigProps.load(results); + expect(props.appId, 'app-id-env'); + expect(props.token, 'token-parsed'); + }); + }); +}