From 2149dc6a6373a81cd255014a3b50ee0bad4f46c2 Mon Sep 17 00:00:00 2001 From: dashenbibi <15820727+dashenbibi@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:19:39 +0800 Subject: [PATCH 1/3] Add macOS release packaging workflow Signed-off-by: dashenbibi <15820727+dashenbibi@users.noreply.github.com> --- .github/workflows/macos-release.yml | 107 ++++++++++++++++++++++++++++ README.md | 2 + README.zh-CN.md | 2 + docs/releasing.md | 35 +++++++++ 4 files changed, 146 insertions(+) create mode 100644 .github/workflows/macos-release.yml create mode 100644 docs/releasing.md diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml new file mode 100644 index 0000000..72f3243 --- /dev/null +++ b/.github/workflows/macos-release.yml @@ -0,0 +1,107 @@ +name: macOS package + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: write + +concurrency: + group: macos-package-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: macOS ${{ matrix.name }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - name: Apple Silicon + runner: macos-15 + target: aarch64-apple-darwin + - name: Intel + runner: macos-15-intel + target: x86_64-apple-darwin + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: apps/desktop/package-lock.json + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + + - name: Verify native runner architecture + env: + EXPECTED_TARGET: ${{ matrix.target }} + run: | + test "$(rustc -vV | sed -n 's/^host: //p')" = "$EXPECTED_TARGET" + + - name: Install frontend dependencies + run: npm ci --prefix apps/desktop + + - name: Verify release tag and signing configuration + if: startsWith(github.ref, 'refs/tags/v') + working-directory: apps/desktop + env: + RELEASE_TAG: ${{ github.ref_name }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + node -e 'const fs = require("fs"); const pkg = require("./package.json"); const tauri = JSON.parse(fs.readFileSync("./src-tauri/tauri.conf.json", "utf8")); const tag = process.env.RELEASE_TAG.replace(/^v/, ""); if (pkg.version !== tag || tauri.version !== tag) { console.error(`tag ${tag}, package ${pkg.version}, tauri ${tauri.version}`); process.exit(1); }' + missing=() + for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!name:-}" ]; then + missing+=("$name") + fi + done + if [ "${#missing[@]}" -ne 0 ]; then + echo "Missing release secrets: ${missing[*]}" >&2 + exit 1 + fi + + - name: Prepare process launcher sidecar + run: ./apps/desktop/scripts/prepare-sidecar.sh release prepare + + - name: Build TestHarbor + uses: tauri-apps/tauri-action@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPLE_CERTIFICATE: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.APPLE_CERTIFICATE || '' }} + APPLE_CERTIFICATE_PASSWORD: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.APPLE_CERTIFICATE_PASSWORD || '' }} + APPLE_SIGNING_IDENTITY: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.APPLE_SIGNING_IDENTITY || '' }} + APPLE_ID: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.APPLE_ID || '' }} + APPLE_PASSWORD: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.APPLE_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ startsWith(github.ref, 'refs/tags/v') && secrets.APPLE_TEAM_ID || '' }} + with: + projectPath: apps/desktop + tauriScript: npm run tauri + args: >- + --target ${{ matrix.target }} + --config src-tauri/tauri.bundle.conf.json + ${{ github.event_name == 'workflow_dispatch' && '--no-sign' || '' }} + uploadWorkflowArtifacts: true + workflowArtifactNamePattern: testharbor-[platform]-[arch]-[bundle] + tagName: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }} + releaseName: ${{ startsWith(github.ref, 'refs/tags/v') && format('TestHarbor {0}', github.ref_name) || '' }} + releaseDraft: true + prerelease: ${{ contains(github.ref_name, '-') }} + generateReleaseNotes: true + releaseAssetNamePattern: TestHarbor_[version]_[arch].[ext] diff --git a/README.md b/README.md index 263a3c4..27c44a1 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ cd apps/desktop npm run tauri:bundle ``` +See [Release packaging](./docs/releasing.md) for GitHub-hosted macOS preview builds and signed draft releases. + ## CLI quick start ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index d8f9002..c9ce44b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -65,6 +65,8 @@ cd apps/desktop npm run tauri:bundle ``` +GitHub 托管的 macOS 预览构建和签名 Draft Release 流程见[发布打包](./docs/releasing.md)。 + ## CLI 快速开始 ```bash diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..0d0f2d2 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,35 @@ +# Release packaging + +TestHarbor uses `.github/workflows/macos-release.yml` for native macOS packaging. + +## Preview build + +Run **macOS package** manually from the GitHub Actions page, or with: + +```bash +gh workflow run macos-release.yml --ref main +``` + +The workflow builds Apple Silicon and Intel packages on matching native runners and uploads them as workflow artifacts. Manual builds pass `--no-sign`; they are for packaging verification only and will trigger macOS trust warnings when downloaded. + +## Draft release + +Before creating a version tag, set these GitHub Actions secrets: + +- `APPLE_CERTIFICATE`: base64-encoded Developer ID Application certificate in PKCS #12 format; +- `APPLE_CERTIFICATE_PASSWORD`: password for that certificate; +- `APPLE_SIGNING_IDENTITY`: certificate signing identity; +- `APPLE_ID`: Apple account used for notarization; +- `APPLE_PASSWORD`: app-specific password for that account; +- `APPLE_TEAM_ID`: Apple Developer team identifier. + +Keep `apps/desktop/package.json` and `apps/desktop/src-tauri/tauri.conf.json` on the same version, then push the matching tag: + +```bash +git tag -s v0.1.0 -m "TestHarbor v0.1.0" +git push origin v0.1.0 +``` + +The workflow rejects a mismatched version or missing release secret. A successful tag build creates a draft GitHub Release and uploads the signed, notarized Apple Silicon and Intel bundles. Review the generated notes and assets before publishing the draft. + +Do not publish manual preview artifacts as a release. Checksums, an SBOM, complete third-party notices, clean-machine installation tests, and final release approval remain separate release gates. From 59718741d6cd5fef464679b7bf224b706f35c7d0 Mon Sep 17 00:00:00 2001 From: dashenbibi <15820727+dashenbibi@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:32:11 +0800 Subject: [PATCH 2/3] Update sidebar brand initials Signed-off-by: dashenbibi <15820727+dashenbibi@users.noreply.github.com> --- apps/desktop/src/WorkspaceSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/WorkspaceSidebar.tsx b/apps/desktop/src/WorkspaceSidebar.tsx index bb60232..3bae78d 100644 --- a/apps/desktop/src/WorkspaceSidebar.tsx +++ b/apps/desktop/src/WorkspaceSidebar.tsx @@ -212,7 +212,7 @@ export function WorkspaceSidebar({ {!sidebarCollapsed && ( <> TestHarbor From 648763058d3eb344e3d8dfab081dd8e8c80e14ae Mon Sep 17 00:00:00 2001 From: dashenbibi <15820727+dashenbibi@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:45:53 +0800 Subject: [PATCH 3/3] fix E2E generation and review workflow --- apps/desktop/src/App.tsx | 90 +++++-- apps/desktop/src/i18n.ts | 4 +- crates/agent-drivers/src/codex.rs | 166 ++++++++++-- crates/application/src/actions.rs | 5 +- crates/application/src/execution.rs | 12 +- crates/application/src/generate.rs | 328 +++++++++++++++++------- crates/application/src/lib.rs | 8 + crates/application/src/result_review.rs | 16 +- crates/application/src/workspace.rs | 308 +++++++++++++++++++++- crates/git-adapter/src/lib.rs | 87 +++++++ crates/review/src/lib.rs | 46 +++- 11 files changed, 925 insertions(+), 145 deletions(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index a2910e2..8fa991f 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -520,6 +520,13 @@ function operationErrorMessage(error: unknown) { lower.includes("start execution blocked") && lower.includes("candidate") ) { summary = tr("The approved isolated worktree changed, so TestHarbor blocked execution. Inspect the changed files; remove temporary files and retry, or regenerate/reapprove changed test source."); + } else if ( + lower.includes("start execution blocked") && + (lower.includes("wrongworkflowstate") || + lower.includes("needs_human") || + lower.includes("needshuman")) + ) { + summary = tr("This task requires human review. Resolve or accept the review findings, approve the case again, and then start formal execution."); } else if (lower.includes("changes outside writable scope")) { summary = tr("The generation service changed files outside the test scope, so TestHarbor blocked the candidate. Restrict output to test files or configure the required test paths/names."); } else if ( @@ -612,6 +619,24 @@ type TestEnvironmentInput = { variables: Record; }; +const DEFAULT_TEST_OBJECTIVE = "编排接口 E2E 流程测试,验证完整业务链路"; +const DEFAULT_ACCEPTANCE_CRITERIA = + "通过 API/接口完成主要操作流程\n查询数据库或缓存验证关键状态变化\n测试数据隔离且按设置清理"; + +function emptyTestEnvironment(): TestEnvironmentInput { + return { + context: "", + base_url: "", + health_url: "", + login_url: "", + startup_command: "", + startup_cwd: "", + username: "", + password: "", + variables: {}, + }; +} + function normDisp(d: string) { const x = d.toLowerCase(); if (x === "mustfix" || x === "must_fix") return "must_fix"; @@ -2090,10 +2115,8 @@ function App() { // Run workflow messages const [log, setLog] = useState([]); - const [objective, setObjective] = useState("编排接口 E2E 流程测试,验证完整业务链路"); - const [criteriaText, setCriteriaText] = useState( - "通过 API/接口完成主要操作流程\n查询数据库或缓存验证关键状态变化\n测试数据隔离且按设置清理", - ); + const [objective, setObjective] = useState(DEFAULT_TEST_OBJECTIVE); + const [criteriaText, setCriteriaText] = useState(DEFAULT_ACCEPTANCE_CRITERIA); const [sourceAnchorsText] = useState(""); const [generationScopeText] = useState(""); const [pastedSpecLabel] = useState("补充说明"); @@ -2101,17 +2124,8 @@ function App() { const [repoMarkdownText] = useState(""); const [testStrategy, setTestStrategy] = useState("e2e_api_flow"); const [cleanupTestData, setCleanupTestData] = useState(true); - const [testEnvironment, setTestEnvironment] = useState({ - context: "", - base_url: "", - health_url: "", - login_url: "", - startup_command: "", - startup_cwd: "", - username: "", - password: "", - variables: {}, - }); + const [testEnvironment, setTestEnvironment] = + useState(emptyTestEnvironment); const [approvedCase, setApprovedCase] = useState(false); const [trusted, setTrusted] = useState(false); const [acceptInfo, setAcceptInfo] = useState(null); @@ -2618,6 +2632,7 @@ function App() { }) : null; if (createdRun) { + resetNewRunForm(); activeRunIdRef.current = createdRun.run_id; setRunId(createdRun.run_id); setCaseDetail(null); @@ -2639,6 +2654,32 @@ function App() { } } + function resetNewRunForm() { + setCaseDetail(null); + setObjective(DEFAULT_TEST_OBJECTIVE); + setCriteriaText(DEFAULT_ACCEPTANCE_CRITERIA); + setTestEnvironment(emptyTestEnvironment()); + setTestStrategy("e2e_api_flow"); + setCleanupTestData(true); + setAgentModel(""); + setReuseAgentSession(false); + setApprovedCase(false); + setTrusted(false); + setAcceptInfo(null); + setAnalysisInfo(null); + setResultReviewInfo(null); + setExecution(null); + setExecutionComplete(false); + setCanAcceptPass(false); + setReadyToApply(false); + setGenerationProgress({ stdout_tail: "", stderr_tail: "" }); + setGenerationRetryAvailable(false); + setRiskDrafts({}); + setReviewDecisionOpen(false); + setEvents([]); + setLog([]); + } + async function pickAndOpen(createRun: boolean) { let path = pathInput.trim(); if (!path || recent.some((item) => item.root_path === path)) { @@ -4503,7 +4544,7 @@ function App() { />

- {t("Free-form text never supplies startup commands or persisted secrets. Use masked runtime inputs declared by the Profile; only keys and fingerprints are stored.")} + {t("Free-form text is saved with this task and sent to the CLI; avoid putting secrets in it. Username, password, and variables are only stored as fingerprints.")}

@@ -5089,7 +5130,10 @@ function App() { {t("Disposition")} onSetDisposition(finding.id, value)} /> @@ -5098,7 +5142,10 @@ function App() { value={ riskDrafts[finding.id] ?? finding.risk_reason ?? "" } - disabled={busy || caseDetail.approved} + disabled={ + busy || + (caseDetail.approved && caseDetail.workflow_state !== "NeedsHuman") + } placeholder={t("Explain why this risk is accepted")} onChange={(event) => setRiskDrafts((previous) => ({ @@ -5143,7 +5190,7 @@ function App() { {t("Generate the next revision")}
- ) : caseDetail.approved || approvedCase ? ( + ) : caseDetail.approved && caseDetail.workflow_state === "ExecutionReady" ? ( <> + ) : caseDetail.approved && caseDetail.workflow_state === "NeedsHuman" ? ( +
+ {t("Human review required")} +

{t("Resolve or accept the review findings, approve the case again, and then start formal execution.")}

+
) : caseDetail.can_approve ? (