Refactor: Update project structure and fix issues
This commit is contained in:
parent
45e79bdebd
commit
c31eed3ed6
29 changed files with 259 additions and 173 deletions
119
CLAUDE.md
Normal file
119
CLAUDE.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# OTO CLI – Guide for AI Assistants
|
||||
|
||||
## Project Overview
|
||||
|
||||
OTO CLI is a Dart command-line tool that executes build/deploy pipelines defined in YAML files.
|
||||
It integrates with Jenkins, runs local test builds, and supports a scheduler mode.
|
||||
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
bin/main.dart
|
||||
└── Application (singleton)
|
||||
└── DataComposer ← parses YAML + Jenkins env
|
||||
└── Pipeline
|
||||
└── Command.byType(CommandType) → Command.execute(DataCommand)
|
||||
```
|
||||
|
||||
1. **`bin/main.dart`** – Entry point. Registers three CLI commands: `template`, `exe`, `scheduler`.
|
||||
2. **`lib/cli/commands/command_exe.dart`** – Parses CLI flags (`-j` Jenkins, `-t` test, `-f` file path) and calls `Application.build()`.
|
||||
3. **`lib/oto/application.dart`** – Singleton. Owns `property` map, `commonData`, `commandStates`. Calls `registerAllCommands()` then runs the pipeline.
|
||||
4. **`lib/oto/core/data_composer.dart`** – Reads YAML and Jenkins env into structured data.
|
||||
5. **`lib/oto/pipeline/pipeline.dart`** – Iterates `DataCommand` list and dispatches to `Command.byType()`.
|
||||
6. **`lib/oto/commands/command_registry.dart`** – Single place where all `CommandType → Command` mappings are registered.
|
||||
|
||||
## BuildType Enum
|
||||
|
||||
Defined in `lib/oto/application.dart`:
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `jenkins` | CI mode – reads workspace and env from Jenkins environment variables |
|
||||
| `test` | Local test mode – uses hardcoded test data |
|
||||
| `file` | Reads pipeline YAML from a local file path |
|
||||
| `scheduler` | Scheduler daemon mode |
|
||||
|
||||
## Tag System (`lib/oto/core/tag_system.dart`)
|
||||
|
||||
Tags are resolved inside YAML values at runtime.
|
||||
|
||||
### Read tag: `<!namespace.key>`
|
||||
Substitutes a value from the runtime store.
|
||||
|
||||
```yaml
|
||||
param:
|
||||
workspace: <!property.workspace> # flat property access
|
||||
version: <!property.build.version> # nested property access
|
||||
state: <!state.myCommandId> # command state (ready/progress/complete)
|
||||
```
|
||||
|
||||
- If the tag occupies the **entire** string, the original type is returned (e.g. a List stays a List).
|
||||
- If the tag is **embedded** in a string, it is converted via `toString()`.
|
||||
|
||||
### Write tag: `<@namespace.key>`
|
||||
Stores the command result back into a property.
|
||||
|
||||
```yaml
|
||||
param:
|
||||
set-version: <@property.appVersion> # saves result into property["appVersion"]
|
||||
```
|
||||
|
||||
Used via `Command.setProperty()` / `TagSystem.setPropertyValue()`.
|
||||
|
||||
## Adding a New Command
|
||||
|
||||
1. **Define data model** in the appropriate `lib/oto/data/*_data.dart` file, extending `DataParam`.
|
||||
2. **Add enum value** to `CommandType` in `lib/oto/commands/command.dart`.
|
||||
3. **Implement command class** extending `Command` with `execute(DataCommand)`.
|
||||
4. **Register** in `lib/oto/commands/command_registry.dart`.
|
||||
5. Run `dart run build_runner build` if you added a new `@JsonSerializable` class.
|
||||
|
||||
## Key Directory Structure
|
||||
|
||||
```
|
||||
lib/
|
||||
├── bin/main.dart # entry point
|
||||
├── cli/commands/ # CLI-layer commands (exe, template, scheduler)
|
||||
└── oto/
|
||||
├── application.dart # singleton orchestrator, BuildType enum
|
||||
├── commands/
|
||||
│ ├── command.dart # Command base class, CommandType enum
|
||||
│ ├── command_registry.dart # all command registrations
|
||||
│ ├── build/ # iOS/Flutter/Dart/MSBuild build commands
|
||||
│ ├── file/ # file operations (copy, rename, delete, zip…)
|
||||
│ ├── git/ # git commands
|
||||
│ ├── infra/ # FTP, SMB
|
||||
│ ├── notification/ # Slack, Mattermost
|
||||
│ ├── process/ # process management
|
||||
│ └── util/ # timers, JSON, string utils
|
||||
├── core/
|
||||
│ ├── tag_system.dart # tag substitution engine
|
||||
│ ├── data_composer.dart # YAML + env → structured data
|
||||
│ └── defined_data.dart # built-in YAML templates
|
||||
├── data/
|
||||
│ ├── base_data.dart # DataParam (base), DataCommon (Jenkins env)
|
||||
│ ├── command_data.dart # DataCommand (re-exports all data types)
|
||||
│ └── *_data.dart # domain-specific data models
|
||||
└── pipeline/
|
||||
└── pipeline.dart # command dispatch loop
|
||||
```
|
||||
|
||||
## Data Model Conventions
|
||||
|
||||
- All command parameters extend `DataParam` (`lib/oto/data/base_data.dart`).
|
||||
- JSON serialization uses `json_annotation`. Generated files are `*.g.dart`.
|
||||
- `*.g.dart` files are normally generated by `dart run build_runner build` but can be edited directly when needed.
|
||||
- `DataCommon` holds Jenkins environment data (workspace, job name, build number, etc.).
|
||||
- `DataCommand` (in `command_data.dart`) is the unified container passed to every `Command.execute()`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- `Application.build()` in `application.dart` uses `catch (e, stacktrace)` (not `on Exception`) to also catch `Error` subtypes such as `TypeError`.
|
||||
- On error, `exit(10)` is called to signal failure to the parent process.
|
||||
|
||||
## Workspace Resolution (Command base class)
|
||||
|
||||
`getWorkspace()` in `command.dart` resolves in this order:
|
||||
1. `workspace` field on the command's data model
|
||||
2. `property['workspace']` in the global property map
|
||||
3. `commonData.workspace` (Jenkins environment)
|
||||
|
|
@ -4,7 +4,7 @@ import 'package:oto_cli/cli/cli.dart';
|
|||
|
||||
class CommandConstant {
|
||||
static _CommandConstantBase? _instance;
|
||||
// ignore: library_private_types_in_public_api
|
||||
// ignore: library_private_types_in_public_api, non_constant_identifier_names
|
||||
static _CommandConstantBase? get OS {
|
||||
if (_instance == null) {
|
||||
if (Platform.isMacOS) {
|
||||
|
|
@ -21,7 +21,6 @@ class CommandConstant {
|
|||
|
||||
class _CommandConstantBase {
|
||||
String get installPath {
|
||||
var userPath = Platform.environment['HOME'];
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class CommandInstall extends CommandBase {
|
|||
}
|
||||
|
||||
Future installFiles(String installPath) async {
|
||||
var file = await File('$installPath/silentbatch.exe').create();
|
||||
await File('$installPath/silentbatch.exe').create();
|
||||
// await file.writeAsBytes(silentbatch);
|
||||
return simpleFuture;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class IsolateScheduler extends IsolateBase {
|
|||
if (hash != fileHash) {
|
||||
updated = true;
|
||||
fileHash = hash;
|
||||
//파일 내용이 바뀔때 마다 새로 읽어 들임
|
||||
// Reload file contents on every change
|
||||
var map = Application.getMapFromYamlA(await file.readAsString());
|
||||
|
||||
//============ Validate yaml format ============//
|
||||
|
|
@ -113,7 +113,7 @@ class IsolateScheduler extends IsolateBase {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
//file이 없을시 종료
|
||||
// Exit if the file no longer exists
|
||||
onError("The file '${file.path}' does not exist.");
|
||||
if (cron != null) await cron?.close();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ class SchedulerManager {
|
|||
}
|
||||
|
||||
Future _startProcess() async {
|
||||
//Launchd로 자동 실행이 되지 않은경우 직접실행
|
||||
// Start directly if Launchd did not auto-launch the process
|
||||
if (_currentScheduler == null) {
|
||||
if (Platform.isWindows) {
|
||||
final program = Platform.resolvedExecutable;
|
||||
|
|
@ -305,7 +305,7 @@ class SchedulerManager {
|
|||
Future start() async {
|
||||
_log('Scheduler is running on pid [$pid].');
|
||||
while (true) {
|
||||
//매초 마다 등록된 파일이 있나 체크
|
||||
// Check every second for registered scheduler files
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
var settings = _localData.map;
|
||||
for (var alias in _schedulers) {
|
||||
|
|
@ -373,7 +373,4 @@ class SchedulerManager {
|
|||
mode: FileMode.append, flush: true);
|
||||
}
|
||||
|
||||
void _logError(Exception e, StackTrace stacktace) {
|
||||
_log('[${e.toString()}]\n${stacktace.toString()}');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class SchedulerOsx extends SchedulerManager {
|
|||
@override
|
||||
String get exePath {
|
||||
var exe = Platform.resolvedExecutable;
|
||||
//homebrew로 설치시 예외 처리
|
||||
// Handle homebrew-installed executable path
|
||||
if (exe.startsWith('/usr/local')) {
|
||||
//intel mac
|
||||
exe = '/usr/local/bin/${path.basenameWithoutExtension(exe)}';
|
||||
|
|
|
|||
|
|
@ -13,10 +13,9 @@ import 'package:oto_cli/oto/commands/command_registry.dart';
|
|||
import 'package:oto_cli/oto/core/defined_data.dart';
|
||||
import 'package:oto_cli/oto/pipeline/pipeline.dart';
|
||||
import 'package:oto_cli/oto/data/command_data.dart';
|
||||
import 'package:oto_cli/oto/data/jenkins_data.dart';
|
||||
import 'package:oto_cli/oto/core/data_composer.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
import 'package:path/path.dart' as pathlib;
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
enum BuildType { jenkins, test, file, scheduler }
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ class Application {
|
|||
if (Application.instance.buildType == BuildType.jenkins) {
|
||||
return Platform.environment['WORKSPACE']!;
|
||||
} else {
|
||||
return pathlib.current;
|
||||
return path.current;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +95,7 @@ class Application {
|
|||
}
|
||||
property = Command.replaceAllTagsMap(property, replace: true);
|
||||
|
||||
//set command in dic, 중복 커맨드 id 필터
|
||||
// Register commands; filter out duplicate command IDs
|
||||
for (var command in build.commands) {
|
||||
if (dataCommandMap.containsKey(command.id)) {
|
||||
var ex = ExceptionData();
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ class BuildiOS extends Command {
|
|||
}
|
||||
|
||||
String? target = data.target;
|
||||
//version이 있을경우 세팅, 없을경우 기존 버전값을 메모리에 가지고 있음
|
||||
// If version is provided, apply it; otherwise retain the existing value from the project
|
||||
if (version != null && version.isNotEmpty) {
|
||||
await updateMarketingVersion(
|
||||
'${data.workspace}/$xcodeproj/project.pbxproj', version);
|
||||
|
|
@ -136,7 +136,7 @@ class BuildiOS extends Command {
|
|||
version = await getSettingXcodeproj(
|
||||
data.workspace!, xcodeproj!, target!, 'MARKETING_VERSION');
|
||||
}
|
||||
//buildNumber가 있을경우 세팅, 없을경우 기존 버전값을 메모리에 가지고 있음
|
||||
// If buildNumber is provided, apply it; otherwise retain the existing value from the project
|
||||
if (buildNumber != null && buildNumber.isNotEmpty) {
|
||||
await updateBuildNumber(
|
||||
'${data.workspace}/$xcodeproj/project.pbxproj', buildNumber);
|
||||
|
|
@ -230,7 +230,7 @@ class BuildiOS extends Command {
|
|||
static Future<File> initializeFastlane(
|
||||
String workspace, String apiKeyPath, String appID) async {
|
||||
var json = jsonDecode(File(apiKeyPath).readAsStringSync());
|
||||
var fastlaneContent = fastlaneTemplete;
|
||||
var fastlaneContent = fastlaneTemplate;
|
||||
fastlaneContent = fastlaneContent
|
||||
.replaceFirst('[APP_IDENTIFIER]', appID)
|
||||
.replaceFirst('[API_KEY_ID]', json['key_id'])
|
||||
|
|
@ -283,7 +283,7 @@ class BuildiOS extends Command {
|
|||
final fileRef = document.findAllElements('FileRef').firstWhere(
|
||||
(element) =>
|
||||
element.getAttribute('location')?.endsWith('.xcodeproj') ?? false,
|
||||
orElse: () => XmlElement(XmlName('Dummy')), // 타입만 맞추고 null 체크는 나중에
|
||||
orElse: () => XmlElement(XmlName('Dummy')), // sentinel for null-check below
|
||||
);
|
||||
|
||||
final location =
|
||||
|
|
@ -338,7 +338,7 @@ class BuildiOS extends Command {
|
|||
}
|
||||
|
||||
if (insideBuildSettings && trimmed == '};') {
|
||||
// 삽입 필요 시 추가
|
||||
// Insert the setting if not already present
|
||||
if (!hasTargetSetting) {
|
||||
updatedLines.add(' $settingKey = $settingValue;');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
var fastlaneTemplete = '''
|
||||
var fastlaneTemplate = '''
|
||||
default_platform(:ios)
|
||||
|
||||
APP_IDENTIFIER = "[APP_IDENTIFIER]"
|
||||
|
|
@ -13,7 +13,7 @@ platform :ios do
|
|||
end
|
||||
|
||||
|
||||
desc "지정한 앱 버전 + 빌드 넘버에 대한 TestFlight 상태 추적 (JSON 출력)"
|
||||
desc "Track TestFlight status for a specific app version and build number (JSON output)"
|
||||
lane :testflight_status do
|
||||
require 'spaceship'
|
||||
require 'json'
|
||||
|
|
@ -43,7 +43,7 @@ platform :ios do
|
|||
end
|
||||
|
||||
|
||||
desc "TestFlight에 등록된 특정 버전의 최신 빌드 넘버 가져오기 (JSON 출력)"
|
||||
desc "Get the latest build number for a specific version registered on TestFlight (JSON output)"
|
||||
lane :latest_build_number do
|
||||
require 'json'
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ platform :ios do
|
|||
end
|
||||
|
||||
|
||||
desc "필터 조건에 맞춰 App Store Connect 버전 상태 JSON 출력"
|
||||
desc "Output App Store Connect version status as JSON based on filter conditions"
|
||||
lane :appstore_versions do
|
||||
require 'spaceship'
|
||||
require 'json'
|
||||
|
|
@ -81,19 +81,19 @@ platform :ios do
|
|||
.select { |v| v.platform == "IOS" }
|
||||
.reject { |v| v.app_store_state == "REPLACED_WITH_NEW_VERSION" }
|
||||
|
||||
# 1. READY_FOR_SALE 중 최신
|
||||
# 1. Latest among READY_FOR_SALE
|
||||
latest_ready = versions
|
||||
.select { |v| v.app_store_state == "READY_FOR_SALE" }
|
||||
.max_by { |v| v.respond_to?(:created_date) ? v.created_date.to_s : "" }
|
||||
|
||||
# 2. PREPARE_FOR_SUBMISSION 중 가장 버전 낮은 것
|
||||
# 2. Lowest version among PREPARE_FOR_SUBMISSION
|
||||
prepare_versions = versions
|
||||
.select { |v| v.app_store_state == "PREPARE_FOR_SUBMISSION" }
|
||||
.sort_by { |v| Gem::Version.new(v.version_string) }
|
||||
|
||||
lowest_prepare = prepare_versions.first
|
||||
|
||||
# 3. 기타 상태 버전들 (READY_FOR_SALE, PREPARE_FOR_SUBMISSION, REPLACED 제외)
|
||||
# 3. Other status versions (excluding READY_FOR_SALE, PREPARE_FOR_SUBMISSION, REPLACED)
|
||||
included_states = ["READY_FOR_SALE", "PREPARE_FOR_SUBMISSION", "REPLACED_WITH_NEW_VERSION"]
|
||||
others = versions.reject { |v| included_states.include?(v.app_store_state) }
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,15 @@ target = project.targets.find { |t| t.name == '{TARGET}' }
|
|||
group = project.main_group
|
||||
file_ref = group.new_file('{ADD_FILE_PATH}')
|
||||
|
||||
# 📌 타겟에 파일 포함
|
||||
# Add file to target
|
||||
build_file = target.add_file_references([file_ref]).first
|
||||
|
||||
# 📌 Copy Bundle Resources에 명시적으로 추가
|
||||
# Explicitly add to Copy Bundle Resources
|
||||
target.resources_build_phase.add_file_reference(file_ref)
|
||||
|
||||
project.save
|
||||
|
||||
puts "✅ {ADD_FILE_PATH} 파일이 프로젝트에 추가되고, Copy Bundle Resources에 포함되었습니다."
|
||||
puts "✅ {ADD_FILE_PATH} has been added to the project and included in Copy Bundle Resources."
|
||||
''';
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import 'package:dart_framework/utils/system_util.dart';
|
|||
import 'package:oto_cli/oto/application.dart';
|
||||
import 'package:oto_cli/oto/core/tag_system.dart';
|
||||
import 'package:oto_cli/oto/data/command_data.dart';
|
||||
import 'package:oto_cli/oto/data/jenkins_data.dart';
|
||||
import 'package:oto_cli/oto/pipeline/pipeline_exe_handle.dart';
|
||||
|
||||
enum CommandType {
|
||||
|
|
@ -148,11 +147,11 @@ abstract class Command {
|
|||
|
||||
String getWorkspace(String? dataWorkspace) {
|
||||
if (dataWorkspace == null) {
|
||||
//모델에 정의된 workspace가 없을경우 property에 설정된 workspace 우선
|
||||
// No workspace on model: prefer workspace from property
|
||||
if (property.containsKey('workspace')) {
|
||||
return getAbs(property['workspace']);
|
||||
}
|
||||
//property에도 설정이 되어 있지 않으면 현재 실행 프로젝트 기준으로 workspace 설정
|
||||
// No workspace in property either: fall back to the current project root
|
||||
else {
|
||||
return getAbs(commonData!.workspace);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,9 @@ import 'package:oto_cli/oto/commands/command.dart';
|
|||
class FTP {
|
||||
final String _host;
|
||||
final int _port;
|
||||
final int _timeout;
|
||||
final String _user;
|
||||
final String _pass;
|
||||
|
||||
bool _isProgress = false;
|
||||
String? _currentRemoteDir;
|
||||
FTPConnect? _ftp;
|
||||
|
||||
|
|
@ -26,8 +24,7 @@ class FTP {
|
|||
: _host = host,
|
||||
_port = port,
|
||||
_user = user,
|
||||
_pass = password,
|
||||
_timeout = timeout {
|
||||
_pass = password {
|
||||
_ftp = FTPConnect(host,
|
||||
port: port, user: user, pass: password, timeout: timeout);
|
||||
}
|
||||
|
|
@ -59,7 +56,6 @@ class FTP {
|
|||
Map<String, FileType> map = {};
|
||||
_currentRemoteDir = null;
|
||||
try {
|
||||
_isProgress = true;
|
||||
var entries = localRemoteMap.entries;
|
||||
for (var entry in entries) {
|
||||
var local = Path.getNormalizePath(entry.key);
|
||||
|
|
@ -79,9 +75,7 @@ class FTP {
|
|||
await _upload(local, remote);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_isProgress = false;
|
||||
}
|
||||
} finally {}
|
||||
}
|
||||
|
||||
Future<void> _upload(String local, String remote) async {
|
||||
|
|
@ -112,13 +106,11 @@ class FTP {
|
|||
var c = Completer();
|
||||
_cache = <String, Map<String, FileType>>{};
|
||||
try {
|
||||
_isProgress = true;
|
||||
var entries = remoteLocalMap.entries;
|
||||
for (var entry in entries) {
|
||||
await _download(entry.key, entry.value);
|
||||
}
|
||||
} finally {
|
||||
_isProgress = false;
|
||||
c.complete();
|
||||
}
|
||||
return c.future;
|
||||
|
|
@ -164,7 +156,6 @@ class FTP {
|
|||
try {
|
||||
await _ftp!.changeDirectory(path);
|
||||
var list = await _ftp!.listDirectoryContent();
|
||||
var count = 0;
|
||||
for (var item in list) {
|
||||
FileType? type;
|
||||
switch (item.type) {
|
||||
|
|
@ -175,15 +166,11 @@ class FTP {
|
|||
type = FileType.file;
|
||||
break;
|
||||
case FTPEntryType.link:
|
||||
// TODO: Handle this case.
|
||||
break;
|
||||
case FTPEntryType.unknown:
|
||||
// TODO: Handle this case.
|
||||
break;
|
||||
}
|
||||
var name = item.name;
|
||||
if (name != '.' && name != '..') {
|
||||
count++;
|
||||
var remotePath = '$path/$name';
|
||||
map[remotePath] = type!;
|
||||
// Application.log('$count = $type = $remotePath');
|
||||
|
|
@ -199,7 +186,6 @@ class FTP {
|
|||
Map<String, FileType>? map;
|
||||
_currentRemoteDir = null;
|
||||
try {
|
||||
_isProgress = true;
|
||||
for (var item in remoteList) {
|
||||
var parent = Path.getParent(item);
|
||||
var name = Path.getName(item);
|
||||
|
|
@ -222,9 +208,7 @@ class FTP {
|
|||
Application.log('Deleted: $item');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_isProgress = false;
|
||||
}
|
||||
} finally {}
|
||||
}
|
||||
|
||||
Future<bool> createFolder(String path) async {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class Upload extends Command {
|
|||
var nameLocal = Path.getName(local);
|
||||
|
||||
if (nameLocal == '*') {
|
||||
//에스테리크(*) 일경우 리모트 폴더를 삭제해서 전체 교체
|
||||
// Wildcard (*): delete the remote folder first for a full replacement
|
||||
//Remove before folder
|
||||
if (remoteMap.containsKey(remote) &&
|
||||
remoteMap[remote] == FileType.directory) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Git extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
//무시가가능한 에러 무시
|
||||
// Ignorable errors are suppressed
|
||||
class GitCommit extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
@ -50,11 +50,11 @@ class GitCommit extends Command {
|
|||
class GitPull extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
var data = DataGitPush.fromJson(getParam(command));
|
||||
var data = DataGitPull.fromJson(getParam(command));
|
||||
|
||||
var shell = StringBuffer(getCDPath(workspace));
|
||||
shell.write(
|
||||
' $and git switch -c ${data.branch!} || git switch ${data.branch!}');
|
||||
' $and git switch -c ${data.branch} || git switch ${data.branch}');
|
||||
shell.write(' $and git pull origin ${data.branch}');
|
||||
|
||||
var process =
|
||||
|
|
@ -64,7 +64,7 @@ class GitPull extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
//무시가가능한 에러 무시
|
||||
// Ignorable errors are suppressed
|
||||
class GitPush extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
@ -86,7 +86,7 @@ class GitPush extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
//로컬에 체크아웃이 없을시 체크아웃하고, 체크아웃이 있을시 이동, 원할시 pull수행
|
||||
// Checks out branch if not present locally; switches to it if it exists; optionally pulls
|
||||
class GitCheckout extends Git {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
@ -122,7 +122,7 @@ class GitCheckout extends Git {
|
|||
}
|
||||
}
|
||||
|
||||
//Git commit에 대한 count 값을 리턴 해준다
|
||||
// Returns the total commit count
|
||||
class GitCount extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
@ -139,7 +139,7 @@ class GitCount extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
//git의 리비전에 대해 돌려준다
|
||||
// Returns the git revision hash
|
||||
class GitRev extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
@ -158,7 +158,7 @@ class GitRev extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
//git 초기화
|
||||
// Resets git working tree to a clean state
|
||||
class GitReset extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
@ -243,7 +243,7 @@ class GitStashApply extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
//무시가가능한 에러 무시
|
||||
// Ignorable errors are suppressed
|
||||
class GitBranch extends Command {
|
||||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class Jira extends Command {
|
|||
slackData.blocks = [];
|
||||
var blockSection = SlackBlockSection();
|
||||
blockSection.text = SlackBlockText();
|
||||
blockSection.text!.text = ':jira: 새 Jira 업무가 할당되었습니다.';
|
||||
blockSection.text!.text = ':jira: A new Jira task has been assigned.';
|
||||
slackData.blocks!.add(blockSection);
|
||||
|
||||
attachment.blocks = [];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import 'package:oto_cli/oto/commands/command.dart';
|
|||
import 'package:oto_cli/oto/data/command_data.dart';
|
||||
import 'package:color/color.dart';
|
||||
import 'package:dart_framework/utils/slack/slack_sender.dart';
|
||||
import 'package:dart_framework/utils/slack/slack_data.dart' as slackData;
|
||||
import 'package:dart_framework/utils/slack/slack_data.dart' as slack_data;
|
||||
|
||||
class Slack extends Command {
|
||||
@override
|
||||
|
|
@ -53,7 +53,7 @@ class SlackFile extends Command {
|
|||
@override
|
||||
Future execute(DataCommand command) async {
|
||||
var data = DataSlackFile.fromJson(getParam(command));
|
||||
var slackFile = slackData.SlackFile();
|
||||
var slackFile = slack_data.SlackFile();
|
||||
slackFile.title = data.title;
|
||||
slackFile.initial_comment = data.initialComment;
|
||||
slackFile.file = File(data.filePath);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import 'package:oto_cli/oto/commands/command.dart';
|
|||
import 'package:oto_cli/oto/data/command_data.dart';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
// ignore: depend_on_referenced_packages
|
||||
// ignore: depend_on_referenced_packages, implementation_imports
|
||||
import 'package:http_parser/src/media_type.dart';
|
||||
|
||||
class WebBase extends Command {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import 'package:dart_framework/platform/process.dart';
|
|||
import 'package:dart_framework/utils/system_util.dart';
|
||||
import 'package:oto_cli/cli/cli.dart';
|
||||
import 'package:oto_cli/oto/application.dart';
|
||||
import 'package:oto_cli/oto/data/jenkins_data.dart';
|
||||
import 'package:oto_cli/oto/data/base_data.dart';
|
||||
import 'package:oto_cli/oto/core/defined_data.dart';
|
||||
|
||||
abstract class DataComposer {
|
||||
|
|
|
|||
|
|
@ -155,11 +155,11 @@ commands:
|
|||
# token: bot-token
|
||||
# channelId: n78a1wrdwjydzdoat6gci8cphe
|
||||
# message:
|
||||
# message: 봇 테스트 메시지입니다.
|
||||
# message: Bot test message.
|
||||
# props:
|
||||
# attachments:
|
||||
# - pretext: Mattermost Bot Test
|
||||
# text: 정상적으로 전송되면 이 메시지가 채널에 표시됩니다.
|
||||
# text: If sent successfully, this message will appear in the channel.
|
||||
|
||||
''';
|
||||
} else if (Platform.isWindows) {
|
||||
|
|
@ -287,11 +287,11 @@ commands:
|
|||
# token: bot-token
|
||||
# channelId: n78a1wrdwjydzdoat6gci8cphe
|
||||
# message:
|
||||
# message: 봇 테스트 메시지입니다.
|
||||
# message: Bot test message.
|
||||
# props:
|
||||
# attachments:
|
||||
# - pretext: Mattermost Bot Test
|
||||
# text: 정상적으로 전송되면 이 메시지가 채널에 표시됩니다.
|
||||
# text: If sent successfully, this message will appear in the channel.
|
||||
|
||||
''';
|
||||
} else if (Platform.isLinux) {}
|
||||
|
|
|
|||
|
|
@ -4,42 +4,42 @@ import 'dart:io';
|
|||
import 'package:dart_framework/utils/path.dart';
|
||||
import 'package:oto_cli/oto/application.dart';
|
||||
|
||||
/// OTO 태그 시스템
|
||||
/// OTO Tag System
|
||||
///
|
||||
/// YAML 파이프라인에서 동적 값을 참조하기 위한 태그 문법을 처리한다.
|
||||
/// Handles the tag syntax used in YAML pipelines to reference dynamic values.
|
||||
///
|
||||
/// ## 읽기 태그 (값 치환)
|
||||
/// ## Read tags (value substitution)
|
||||
///
|
||||
/// ```yaml
|
||||
/// exe: BuildiOS
|
||||
/// param:
|
||||
/// workspace: <!property.workspace> # property 직접 접근
|
||||
/// version: <!property.build.version> # property 중첩 접근
|
||||
/// state: <!state.buildStep> # commandStates 접근 (ready/progress/complete)
|
||||
/// workspace: <!property.workspace> # direct property access
|
||||
/// version: <!property.build.version> # nested property access
|
||||
/// state: <!state.buildStep> # commandStates access (ready/progress/complete)
|
||||
/// ```
|
||||
///
|
||||
/// 태그가 문자열 전체인 경우 원본 타입 그대로 반환 (String→Any).
|
||||
/// 문자열 일부인 경우 toString()으로 삽입.
|
||||
/// When a tag occupies the entire string, the original type is returned (String→Any).
|
||||
/// When a tag is embedded within a string, it is inserted via toString().
|
||||
///
|
||||
/// ## 쓰기 태그 (값 저장)
|
||||
/// ## Write tags (value storage)
|
||||
///
|
||||
/// ```yaml
|
||||
/// exe: SetValue
|
||||
/// param:
|
||||
/// set-version: <@property.appVersion> # property에 값 저장
|
||||
/// set-version: <@property.appVersion> # store value into property
|
||||
/// ```
|
||||
///
|
||||
/// `setPropertyValue()` 또는 `setProperty()` 호출 시 사용.
|
||||
/// Used when calling `setPropertyValue()` or `setProperty()`.
|
||||
class TagSystem {
|
||||
/// 읽기 태그 패턴: `<!namespace.key>`
|
||||
/// Read tag pattern: `<!namespace.key>`
|
||||
static RegExp regEx = RegExp(r'(<!)([_a-zA-Z0-9.-]+)(>)');
|
||||
|
||||
/// 쓰기 태그 패턴: `<@namespace.key>`
|
||||
/// Write tag pattern: `<@namespace.key>`
|
||||
static RegExp regSetEx = RegExp(r'(<@)([_a-zA-Z0-9.-]+)(>)');
|
||||
|
||||
/// Map의 모든 키/값에서 태그를 재귀적으로 치환한다.
|
||||
/// Recursively replaces tags in all keys and values of a Map.
|
||||
///
|
||||
/// [replace]가 true이면 원본 [map]을 직접 수정하고 반환한다.
|
||||
/// If [replace] is true, modifies and returns the original [map] in place.
|
||||
static Map<String, dynamic> replaceAllTagsMap(Map<String, dynamic> map,
|
||||
{bool replace = false}) {
|
||||
var newMap = <String, dynamic>{};
|
||||
|
|
@ -73,7 +73,7 @@ class TagSystem {
|
|||
return newMap;
|
||||
}
|
||||
|
||||
/// List의 모든 요소에서 태그를 재귀적으로 치환한다.
|
||||
/// Recursively replaces tags in all elements of a List.
|
||||
static List replaceAllTagsList(List list, {bool replace = false}) {
|
||||
var newList = <dynamic>[];
|
||||
for (var i = 0; i < list.length; ++i) {
|
||||
|
|
@ -96,7 +96,7 @@ class TagSystem {
|
|||
return newList;
|
||||
}
|
||||
|
||||
/// 쓰기 태그(`<@property.key>`)로 지정된 property에 값을 저장한다.
|
||||
/// Stores a value into the property specified by a write tag (`<@property.key>`).
|
||||
static Future setPropertyValue(String tag, dynamic value,
|
||||
{bool? showPrint}) async {
|
||||
var match = regSetEx.firstMatch(tag);
|
||||
|
|
@ -125,10 +125,10 @@ class TagSystem {
|
|||
return Future.value();
|
||||
}
|
||||
|
||||
/// 태그 이름으로 실제 값을 조회한다.
|
||||
/// Looks up the actual value for a tag name.
|
||||
///
|
||||
/// - `property.key` → `Application.instance.property[key]`
|
||||
/// - `property.nested.key` → 중첩 Map 순회
|
||||
/// - `property.nested.key` → traverses nested Maps
|
||||
/// - `state.commandId` → `Application.instance.commandStates[commandId]`
|
||||
static dynamic _getTagValue(String value) {
|
||||
var setted = false;
|
||||
|
|
@ -183,7 +183,7 @@ class TagSystem {
|
|||
return data;
|
||||
}
|
||||
|
||||
/// 상대경로(`.`, `..`)를 절대경로로 변환한다. 경로 내 태그도 치환된다.
|
||||
/// Converts a relative path (`.`, `..`) to an absolute path, also resolving any tags within it.
|
||||
static String getAbs(String path) {
|
||||
path = getPathNormal(path);
|
||||
if (path == '.' || path.startsWith('./') || path.startsWith('.\\')) {
|
||||
|
|
@ -196,15 +196,15 @@ class TagSystem {
|
|||
return File(path).absolute.path;
|
||||
}
|
||||
|
||||
/// 경로 구분자를 정규화하고 태그를 치환한다.
|
||||
/// Normalizes path separators and resolves tags within the path.
|
||||
static String getPathNormal(String path) {
|
||||
return replaceTagValue(Path.getNormal(path));
|
||||
}
|
||||
|
||||
/// 문자열에서 태그(`<!...>`)를 치환한다.
|
||||
/// Replaces tags (`<!...>`) within a string.
|
||||
///
|
||||
/// 태그가 문자열 전체인 경우 원본 타입(dynamic)을 반환한다.
|
||||
/// 태그가 문자열 일부인 경우 문자열로 삽입한다.
|
||||
/// If the tag occupies the entire string, returns the original type (dynamic).
|
||||
/// If the tag is embedded within a string, it is inserted as a string.
|
||||
static dynamic replaceTagValue(String raw) {
|
||||
var list = regEx.allMatches(raw);
|
||||
if (list.length == 1) {
|
||||
|
|
@ -223,7 +223,7 @@ class TagSystem {
|
|||
return raw;
|
||||
}
|
||||
|
||||
/// 태그 하나만 포함된 문자열에서 값을 원본 타입으로 반환한다.
|
||||
/// Returns the value in its original type from a string that contains exactly one tag.
|
||||
static dynamic replaceSingleTagValue(String raw) {
|
||||
dynamic value;
|
||||
var list = regEx.allMatches(raw);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,28 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
|
||||
part 'base_data.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DataCommon {
|
||||
late String workspace;
|
||||
late String jenkinsHome;
|
||||
late String jenkinsUrl;
|
||||
late String buildUrl;
|
||||
late String jobUrl;
|
||||
late String jobName;
|
||||
late String gitCommit;
|
||||
late int buildNumber;
|
||||
late int buildID;
|
||||
late String buildDisplayName;
|
||||
late String gitBranch;
|
||||
late String buildTag;
|
||||
|
||||
DataCommon();
|
||||
|
||||
factory DataCommon.fromJson(Map<String, dynamic> json) =>
|
||||
_$DataCommonFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$DataCommonToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class DataParam {
|
||||
DataParam();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,36 @@ part of 'base_data.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DataCommon _$DataCommonFromJson(Map<String, dynamic> json) => DataCommon()
|
||||
..workspace = json['workspace'] as String
|
||||
..jenkinsHome = json['jenkinsHome'] as String
|
||||
..jenkinsUrl = json['jenkinsUrl'] as String
|
||||
..buildUrl = json['buildUrl'] as String
|
||||
..jobUrl = json['jobUrl'] as String
|
||||
..jobName = json['jobName'] as String
|
||||
..gitCommit = json['gitCommit'] as String
|
||||
..buildNumber = (json['buildNumber'] as num).toInt()
|
||||
..buildID = (json['buildID'] as num).toInt()
|
||||
..buildDisplayName = json['buildDisplayName'] as String
|
||||
..gitBranch = json['gitBranch'] as String
|
||||
..buildTag = json['buildTag'] as String;
|
||||
|
||||
Map<String, dynamic> _$DataCommonToJson(DataCommon instance) =>
|
||||
<String, dynamic>{
|
||||
'workspace': instance.workspace,
|
||||
'jenkinsHome': instance.jenkinsHome,
|
||||
'jenkinsUrl': instance.jenkinsUrl,
|
||||
'buildUrl': instance.buildUrl,
|
||||
'jobUrl': instance.jobUrl,
|
||||
'jobName': instance.jobName,
|
||||
'gitCommit': instance.gitCommit,
|
||||
'buildNumber': instance.buildNumber,
|
||||
'buildID': instance.buildID,
|
||||
'buildDisplayName': instance.buildDisplayName,
|
||||
'gitBranch': instance.gitBranch,
|
||||
'buildTag': instance.buildTag,
|
||||
};
|
||||
|
||||
DataParam _$DataParamFromJson(Map<String, dynamic> json) => DataParam()
|
||||
..workspace = json['workspace'] as String?
|
||||
..passExitCodes = (json['passExitCodes'] as List<dynamic>?)
|
||||
|
|
|
|||
|
|
@ -126,11 +126,11 @@ class DataBuildiOS extends DataParam {
|
|||
late Map<String, String>? settings;
|
||||
|
||||
//Version Setting
|
||||
late String? target; //없을시 xcodeproj 자동 검색하여 최상단 target을 가져옴
|
||||
late String? version; //존재시 autoBuildNumber = false
|
||||
late String? buildNumber; //존재시 autoBuildNumber = false
|
||||
late String? target; // if null, auto-detects the top-level target from xcodeproj
|
||||
late String? version; // if set, disables autoBuildNumber
|
||||
late String? buildNumber; // if set, disables autoBuildNumber
|
||||
late bool? autoBuildNumber = false;
|
||||
late String? apiKeyPath; //autoVersionNumber 사용시 필수
|
||||
late String? apiKeyPath; // required when using autoVersionNumber
|
||||
|
||||
//set
|
||||
late String? setVersion;
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'jenkins_data.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class DataCommon {
|
||||
late String workspace;
|
||||
late String jenkinsHome;
|
||||
late String jenkinsUrl;
|
||||
late String buildUrl;
|
||||
late String jobUrl;
|
||||
late String jobName;
|
||||
late String gitCommit;
|
||||
late int buildNumber;
|
||||
late int buildID;
|
||||
late String buildDisplayName;
|
||||
late String gitBranch;
|
||||
late String buildTag;
|
||||
|
||||
DataCommon();
|
||||
|
||||
factory DataCommon.fromJson(Map<String, dynamic> json) =>
|
||||
_$DataCommonFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$DataCommonToJson(this);
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'jenkins_data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DataCommon _$DataCommonFromJson(Map<String, dynamic> json) => DataCommon()
|
||||
..workspace = json['workspace'] as String
|
||||
..jenkinsHome = json['jenkinsHome'] as String
|
||||
..jenkinsUrl = json['jenkinsUrl'] as String
|
||||
..buildUrl = json['buildUrl'] as String
|
||||
..jobUrl = json['jobUrl'] as String
|
||||
..jobName = json['jobName'] as String
|
||||
..gitCommit = json['gitCommit'] as String
|
||||
..buildNumber = (json['buildNumber'] as num).toInt()
|
||||
..buildID = (json['buildID'] as num).toInt()
|
||||
..buildDisplayName = json['buildDisplayName'] as String
|
||||
..gitBranch = json['gitBranch'] as String
|
||||
..buildTag = json['buildTag'] as String;
|
||||
|
||||
Map<String, dynamic> _$DataCommonToJson(DataCommon instance) =>
|
||||
<String, dynamic>{
|
||||
'workspace': instance.workspace,
|
||||
'jenkinsHome': instance.jenkinsHome,
|
||||
'jenkinsUrl': instance.jenkinsUrl,
|
||||
'buildUrl': instance.buildUrl,
|
||||
'jobUrl': instance.jobUrl,
|
||||
'jobName': instance.jobName,
|
||||
'gitCommit': instance.gitCommit,
|
||||
'buildNumber': instance.buildNumber,
|
||||
'buildID': instance.buildID,
|
||||
'buildDisplayName': instance.buildDisplayName,
|
||||
'gitBranch': instance.gitBranch,
|
||||
'buildTag': instance.buildTag,
|
||||
};
|
||||
|
|
@ -13,9 +13,9 @@ class DataSlackSender extends DataParam {
|
|||
DataSlackSender();
|
||||
|
||||
late String token;
|
||||
late dynamic property; //oto 내부 생성된 object의 경우
|
||||
late Map<String, dynamic>? message; //yaml로 선언된 값
|
||||
late String? messageJson; //json값으로 선언된 값
|
||||
late dynamic property; // object created internally by oto
|
||||
late Map<String, dynamic>? message; // value declared in YAML
|
||||
late String? messageJson; // value declared as JSON string
|
||||
late Map<String, String>? idMap;
|
||||
|
||||
factory DataSlackSender.fromJson(Map<String, dynamic> json) =>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ class DataExeHandle implements DataValidator {
|
|||
|
||||
factory DataExeHandle.fromJson(Map<String, dynamic> json) =>
|
||||
_$DataExeHandleFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$DataExeHandleToJson(this);
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +161,6 @@ class DataContain extends DataValidator {
|
|||
|
||||
factory DataContain.fromJson(Map<String, dynamic> json) =>
|
||||
_$DataContainFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$DataContainToJson(this);
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +223,6 @@ class DataForeach extends DataValidator {
|
|||
|
||||
factory DataForeach.fromJson(Map<String, dynamic> json) =>
|
||||
_$DataForeachFromJson(json);
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$DataForeachToJson(this);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,11 +103,11 @@ class DataStringReplacePattern extends DataParam {
|
|||
DataStringReplacePattern();
|
||||
|
||||
int? startAt = 0;
|
||||
late String startPattern; //시작 패턴
|
||||
late String endPattern; //종료 패턴
|
||||
late String text; //원본 텍스트
|
||||
late String value; //교체될 값
|
||||
late String setValue; //결과 저장소
|
||||
late String startPattern; // start delimiter pattern
|
||||
late String endPattern; // end delimiter pattern
|
||||
late String text; // original text to process
|
||||
late String value; // replacement value
|
||||
late String setValue; // property key to store the result
|
||||
|
||||
factory DataStringReplacePattern.fromJson(Map<String, dynamic> json) =>
|
||||
_$DataStringReplacePatternFromJson(json);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ environment:
|
|||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
# path: ^1.8.0
|
||||
path: ^1.8.0
|
||||
http: ^1.2.0
|
||||
json_annotation: ^4.8.1
|
||||
resource_importer: ^0.2.0
|
||||
dart_framework:
|
||||
|
|
|
|||
Loading…
Reference in a new issue