From d048d7ec9cde9086c5a99286ab9971c3f65fe792 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 12:41:08 +0800 Subject: [PATCH 1/2] ci: backport release/* source validation workflow to release/v1.1.5 (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 让现有 release/v1.1.5 分支自身包含门禁 workflow(pull_request_target 用 base 分支版本)。 详见 iac_modules/docs/tldr-github-branch-model.md Co-authored-by: Haitao Pan Co-authored-by: Claude Opus 4.8 --- .github/workflows/validate-release-pr.yml | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/validate-release-pr.yml diff --git a/.github/workflows/validate-release-pr.yml b/.github/workflows/validate-release-pr.yml new file mode 100644 index 00000000..7af78740 --- /dev/null +++ b/.github/workflows/validate-release-pr.yml @@ -0,0 +1,44 @@ +name: Validate Release PR + +# release/* 分支的发布策略门禁:仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。 +# 详见 iac_modules/docs/tldr-github-branch-model.md +on: + pull_request_target: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + pull-requests: read + +jobs: + validate-release-source: + runs-on: ubuntu-latest + if: startsWith(github.base_ref, 'release/') + steps: + - name: Check PR source branch + run: | + SRC="${{ github.head_ref }}" + TGT="${{ github.base_ref }}" + LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}" + + echo "🔍 Validating PR into release branch" + echo " source: $SRC" + echo " target: $TGT" + echo " labels: $LABELS" + + if [[ "$SRC" =~ ^hotfix/ ]]; then + echo "✅ Allowed: hotfix/* branch" + exit 0 + fi + + if [[ "$LABELS" =~ (^|,)(cherry-pick|backport)(,|$) ]]; then + echo "✅ Allowed: cherry-pick/backport labeled PR" + exit 0 + fi + + echo "❌ Rejected." + echo "release/* 仅接受:" + echo " - 来自 hotfix/* 的 PR" + echo " - 带 cherry-pick 或 backport 标签的 PR(已验证 feature 的 backport/cherry-pick)" + echo "禁止从 main / develop / feature/* 直接合并到 release/*。" + exit 1 From b1ec29fa369e844f72bbea284042b91db4ac53d3 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 15:44:29 +0800 Subject: [PATCH 2/2] fix: reveal artifact files without blocking (#24) Co-authored-by: Haitao Pan --- .../assistant_page_state_closure.dart | 17 +---- lib/runtime/local_file_revealer.dart | 39 +++++++++++ test/runtime/local_file_revealer_test.dart | 64 +++++++++++++++++++ 3 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 lib/runtime/local_file_revealer.dart create mode 100644 test/runtime/local_file_revealer_test.dart diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index 97fe7850..fa3c7e9a 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -17,6 +17,7 @@ import '../../app/ui_feature_manifest.dart'; import '../../i18n/app_language.dart'; import '../../models/app_models.dart'; import '../../runtime/gateway_acp_client.dart'; +import '../../runtime/local_file_revealer.dart'; import '../../runtime/runtime_models.dart'; import '../../theme/app_palette.dart'; import '../../theme/app_theme.dart'; @@ -404,21 +405,7 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal { entry.relativePath.contains(':\\') ? entry.relativePath : '${workspacePath.replaceAll(RegExp(r'[\\/]+$'), '')}${Platform.pathSeparator}${entry.relativePath}'; - if (Platform.isMacOS) { - await Process.run('open', ['-R', targetPath]); - return; - } - if (Platform.isLinux) { - await Process.run('xdg-open', [ - File(targetPath).parent.path, - ]); - return; - } - if (Platform.isWindows) { - await Process.run('explorer.exe', [ - '/select,$targetPath', - ]); - } + await revealLocalFile(targetPath); }, loadSnapshot: () => controller.loadAssistantArtifactSnapshot( sessionKey: activeSessionKey, diff --git a/lib/runtime/local_file_revealer.dart b/lib/runtime/local_file_revealer.dart new file mode 100644 index 00000000..53e09733 --- /dev/null +++ b/lib/runtime/local_file_revealer.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +typedef DetachedProcessLauncher = + Future Function( + String executable, + List arguments, { + required ProcessStartMode mode, + }); + +Future revealLocalFile( + String targetPath, { + String? operatingSystem, + DetachedProcessLauncher? launchDetached, +}) async { + final launcher = launchDetached ?? _launchDetached; + switch (operatingSystem ?? Platform.operatingSystem) { + case 'macos': + await launcher('open', [ + '-R', + targetPath, + ], mode: ProcessStartMode.detached); + case 'linux': + await launcher('xdg-open', [ + File(targetPath).parent.path, + ], mode: ProcessStartMode.detached); + case 'windows': + await launcher('explorer.exe', [ + '/select,$targetPath', + ], mode: ProcessStartMode.detached); + } +} + +Future _launchDetached( + String executable, + List arguments, { + required ProcessStartMode mode, +}) async { + await Process.start(executable, arguments, mode: mode); +} diff --git a/test/runtime/local_file_revealer_test.dart b/test/runtime/local_file_revealer_test.dart new file mode 100644 index 00000000..a935fe9f --- /dev/null +++ b/test/runtime/local_file_revealer_test.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/local_file_revealer.dart'; + +void main() { + group('revealLocalFile', () { + test('reveals the file in Finder on macOS', () async { + final invocation = await _captureInvocation( + operatingSystem: 'macos', + targetPath: '/tmp/thread/report.pdf', + ); + + expect(invocation.executable, 'open'); + expect(invocation.arguments, ['-R', '/tmp/thread/report.pdf']); + expect(invocation.mode, ProcessStartMode.detached); + }); + + test('opens the parent directory on Linux', () async { + final invocation = await _captureInvocation( + operatingSystem: 'linux', + targetPath: '/tmp/thread/reports/report.pdf', + ); + + expect(invocation.executable, 'xdg-open'); + expect(invocation.arguments, ['/tmp/thread/reports']); + expect(invocation.mode, ProcessStartMode.detached); + }); + + test('selects the file in Explorer on Windows', () async { + final invocation = await _captureInvocation( + operatingSystem: 'windows', + targetPath: r'C:\thread\report.pdf', + ); + + expect(invocation.executable, 'explorer.exe'); + expect(invocation.arguments, [r'/select,C:\thread\report.pdf']); + expect(invocation.mode, ProcessStartMode.detached); + }); + }); +} + +Future<_ProcessInvocation> _captureInvocation({ + required String operatingSystem, + required String targetPath, +}) async { + late _ProcessInvocation invocation; + await revealLocalFile( + targetPath, + operatingSystem: operatingSystem, + launchDetached: (executable, arguments, {required mode}) async { + invocation = _ProcessInvocation(executable, arguments, mode); + }, + ); + return invocation; +} + +class _ProcessInvocation { + const _ProcessInvocation(this.executable, this.arguments, this.mode); + + final String executable; + final List arguments; + final ProcessStartMode mode; +}