Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/workflows/macos-release.yml
Original file line number Diff line number Diff line change
@@ -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]
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ cd apps/desktop
npm run tauri:bundle
```

GitHub 托管的 macOS 预览构建和签名 Draft Release 流程见[发布打包](./docs/releasing.md)

## CLI 快速开始

```bash
Expand Down
90 changes: 71 additions & 19 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -612,6 +619,24 @@ type TestEnvironmentInput = {
variables: Record<string, string>;
};

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";
Expand Down Expand Up @@ -2090,28 +2115,17 @@ function App() {

// Run workflow messages
const [log, setLog] = useState<string[]>([]);
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("补充说明");
const [pastedSpecBody] = useState("");
const [repoMarkdownText] = useState("");
const [testStrategy, setTestStrategy] = useState("e2e_api_flow");
const [cleanupTestData, setCleanupTestData] = useState(true);
const [testEnvironment, setTestEnvironment] = useState<TestEnvironmentInput>({
context: "",
base_url: "",
health_url: "",
login_url: "",
startup_command: "",
startup_cwd: "",
username: "",
password: "",
variables: {},
});
const [testEnvironment, setTestEnvironment] =
useState<TestEnvironmentInput>(emptyTestEnvironment);
const [approvedCase, setApprovedCase] = useState(false);
const [trusted, setTrusted] = useState(false);
const [acceptInfo, setAcceptInfo] = useState<string | null>(null);
Expand Down Expand Up @@ -2618,6 +2632,7 @@ function App() {
})
: null;
if (createdRun) {
resetNewRunForm();
activeRunIdRef.current = createdRun.run_id;
setRunId(createdRun.run_id);
setCaseDetail(null);
Expand All @@ -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)) {
Expand Down Expand Up @@ -4503,7 +4544,7 @@ function App() {
/>
</label>
<p className="environment-help">
{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.")}
</p>
</details>
<div className="composer-options test-strategy-options">
Expand Down Expand Up @@ -5089,7 +5130,10 @@ function App() {
<span>{t("Disposition")}</span>
<ReviewDispositionSelect
value={disposition}
disabled={busy || caseDetail.approved}
disabled={
busy ||
(caseDetail.approved && caseDetail.workflow_state !== "NeedsHuman")
}
onChange={(value) => onSetDisposition(finding.id, value)}
/>
</label>
Expand All @@ -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) => ({
Expand Down Expand Up @@ -5143,7 +5190,7 @@ function App() {
{t("Generate the next revision")} <span>→</span>
</button>
</div>
) : caseDetail.approved || approvedCase ? (
) : caseDetail.approved && caseDetail.workflow_state === "ExecutionReady" ? (
<>
<button
type="button"
Expand All @@ -5154,6 +5201,11 @@ function App() {
{t("Start formal execution")} <span>→</span>
</button>
</>
) : caseDetail.approved && caseDetail.workflow_state === "NeedsHuman" ? (
<div className="block-reasons human-review-required">
<strong>{t("Human review required")}</strong>
<p>{t("Resolve or accept the review findings, approve the case again, and then start formal execution.")}</p>
</div>
) : caseDetail.can_approve ? (
<button
type="button"
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/WorkspaceSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export function WorkspaceSidebar({
{!sidebarCollapsed && (
<>
<span className="product-mark" aria-hidden="true">
ST
TH
</span>
<span className="sidebar-brand-copy">
<strong>TestHarbor</strong>
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ const zhCN: Record<string, string> = {
"Optional": "可选",
"Environment information": "环境信息",
"Example:\nService URL: http://localhost:8300\nLogin endpoint: POST /api/login\nVerify MySQL and Redis state changes; test tenant: qa": "例如:\n服务地址:http://localhost:8300\n登录接口:POST /api/login\n需要验证 MySQL 和 Redis 中的状态变化,测试租户为 qa",
"Free-form text never supplies startup commands or persisted secrets. Use masked runtime inputs declared by the Profile; only keys and fingerprints are stored.": "不会从自由文本提取或执行启动命令,也不会持久化自由文本。秘密应通过 Profile 声明的 masked runtime input 提供,数据库只记录键与指纹。",
"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.": "自由文本会随任务保存并发送给 CLI,请勿填写明文密钥;账号、密码和变量只保存指纹。",
"Test strategy": "测试策略",
"API E2E flow test": "接口 E2E 流程测试",
"Clean up test data after case execution": "用例执行后清理测试数据",
Expand Down Expand Up @@ -331,6 +331,8 @@ const zhCN: Record<string, string> = {
"Changes required": "需要修改",
"Blocked": "阻塞",
"Human review required": "需要人工判断",
"This task requires human review. Resolve or accept the review findings, approve the case again, and then start formal execution.": "当前任务需要人工判断。请先处理或接受评审发现,重新批准用例后再开始正式执行。",
"Resolve or accept the review findings, approve the case again, and then start formal execution.": "请先处理或接受评审发现,重新批准用例后再开始正式执行。",
"Accept fail": "接受失败",
"Critical": "严重",
"High risk": "高风险",
Expand Down
Loading
Loading