apphx 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -138
- package/benchmarks/peer-context-worker.mjs +186 -0
- package/benchmarks/peer-context.mjs +150 -0
- package/dist/apple-types.js.map +1 -1
- package/dist/cli.js +373 -121
- package/dist/cli.js.map +1 -1
- package/dist/config.js +42 -4
- package/dist/config.js.map +1 -1
- package/dist/dependencies.js +455 -16
- package/dist/dependencies.js.map +1 -1
- package/dist/export-options.js +44 -2
- package/dist/export-options.js.map +1 -1
- package/dist/flutter.js +13 -25
- package/dist/flutter.js.map +1 -1
- package/dist/go-dependencies.js +389 -0
- package/dist/go-dependencies.js.map +1 -0
- package/dist/inbox.js +7 -8
- package/dist/inbox.js.map +1 -1
- package/dist/integrations.js +29 -29
- package/dist/integrations.js.map +1 -1
- package/dist/isolation.js +190 -0
- package/dist/isolation.js.map +1 -0
- package/dist/macos.js +57 -0
- package/dist/macos.js.map +1 -0
- package/dist/mygo-build.js +55 -0
- package/dist/mygo-build.js.map +1 -0
- package/dist/policy.js +35 -0
- package/dist/policy.js.map +1 -0
- package/dist/preflight.js +1 -1
- package/dist/process.js +3 -3
- package/dist/project-profiles.js +163 -0
- package/dist/project-profiles.js.map +1 -0
- package/dist/proof.js +33 -0
- package/dist/proof.js.map +1 -0
- package/dist/remote.js +1 -1
- package/dist/runner.js +269 -287
- package/dist/runner.js.map +1 -1
- package/dist/scheduler.js +0 -3
- package/dist/scheduler.js.map +1 -1
- package/dist/schemas.js +156 -44
- package/dist/schemas.js.map +1 -1
- package/dist/store.js +166 -44
- package/dist/store.js.map +1 -1
- package/dist/vendor/obfuscator/dist/cli.js +72 -0
- package/dist/vendor/obfuscator/dist/cli.js.map +1 -0
- package/dist/vendor/obfuscator/dist/code.js +585 -0
- package/dist/vendor/obfuscator/dist/code.js.map +1 -0
- package/dist/vendor/obfuscator/dist/common.js +262 -0
- package/dist/vendor/obfuscator/dist/common.js.map +1 -0
- package/dist/vendor/obfuscator/dist/config.js +123 -0
- package/dist/vendor/obfuscator/dist/config.js.map +1 -0
- package/dist/vendor/obfuscator/dist/content.js +89 -0
- package/dist/vendor/obfuscator/dist/content.js.map +1 -0
- package/dist/vendor/obfuscator/dist/obfuscate.js +93 -0
- package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -0
- package/dist/vendor/obfuscator/dist/png.js +283 -0
- package/dist/vendor/obfuscator/dist/png.js.map +1 -0
- package/dist/vendor/obfuscator/dist/project.js +159 -0
- package/dist/vendor/obfuscator/dist/project.js.map +1 -0
- package/dist/vendor/obfuscator/dist/resources.js +335 -0
- package/dist/vendor/obfuscator/dist/resources.js.map +1 -0
- package/dist/vendor/obfuscator/package.json +22 -0
- package/dist/workspace.js +113 -0
- package/dist/workspace.js.map +1 -0
- package/evals/control.mjs +73 -0
- package/evals/expectations.mjs +37 -0
- package/evals/host.mjs +51 -0
- package/evals/oracle.mjs +80 -0
- package/evals/policy.mjs +268 -0
- package/examples/android-java.factory.json +18 -0
- package/examples/android-java.json +16 -0
- package/examples/existing-project.factory.json +18 -0
- package/examples/existing-project.json +17 -0
- package/examples/expo.eas.json +17 -0
- package/examples/expo.factory.json +18 -0
- package/examples/expo.json +16 -0
- package/examples/factory.json +0 -2
- package/examples/flutter.factory.json +13 -0
- package/examples/flutter.json +16 -0
- package/examples/luggage.json +5 -3
- package/examples/mygo-web.factory.json +18 -0
- package/examples/mygo-web.json +16 -0
- package/examples/mygo.factory.json +18 -0
- package/examples/mygo.json +16 -0
- package/examples/react-native.factory.json +18 -0
- package/examples/react-native.json +16 -0
- package/examples/remote.json +1 -1
- package/examples/runtime/android-java/app/build.gradle +7 -0
- package/examples/runtime/android-java/app/src/main/AndroidManifest.xml +8 -0
- package/examples/runtime/android-java/app/src/main/java/dev/apphx/runtime/MainActivity.java +44 -0
- package/examples/runtime/android-java/sdk-build.mjs +29 -0
- package/examples/runtime/android-java/settings.gradle +2 -0
- package/examples/runtime/android-ui.mjs +70 -0
- package/examples/runtime/mygo-native/go.mod +13 -0
- package/examples/runtime/mygo-native/go.sum +10 -0
- package/examples/runtime/mygo-native/main.go +173 -0
- package/examples/runtime/mygo-native/main_test.go +14 -0
- package/examples/runtime/mygo-native/mygo.json +1 -0
- package/examples/runtime/mygo-web/bun.lock +15 -0
- package/examples/runtime/mygo-web/go.mod +9 -0
- package/examples/runtime/mygo-web/go.sum +8 -0
- package/examples/runtime/mygo-web/index.html +1 -0
- package/examples/runtime/mygo-web/main.go +60 -0
- package/examples/runtime/mygo-web/mygo.config.ts +10 -0
- package/examples/runtime/mygo-web/package.json +1 -0
- package/examples/runtime/mygo-web/src/main.ts +23 -0
- package/examples/runtime/mygo-web/src/mygo.ts +14 -0
- package/examples/swift.factory.json +18 -0
- package/examples/swift.json +16 -0
- package/examples/uni-app.factory.json +18 -0
- package/examples/uni-app.json +16 -0
- package/package.json +12 -5
- package/scripts/bundle-obfuscator.mjs +19 -0
- package/scripts/runtime-smoke.mjs +209 -0
- package/skills/apphx/SKILL.md +180 -0
- package/skills/apphx/resources/google-play-research-result.json +6 -0
- package/skills/apphx/resources/local-research-result.json +6 -0
- package/skills/{apple-app-factory → apphx}/resources/requirement.json +5 -8
- package/skills/apphx/resources/research-result.json +12 -0
- package/skills/apphx/resources/review-result.json +7 -0
- package/dist/apple.js +0 -900
- package/dist/apple.js.map +0 -1
- package/skills/apple-app-factory/SKILL.md +0 -103
- package/skills/apple-app-factory/resources/research-result.json +0 -21
- package/skills/apple-app-factory/resources/review-result.json +0 -7
- /package/skills/{apple-app-factory → apphx}/LICENSE +0 -0
package/dist/runner.js
CHANGED
|
@@ -1,106 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { validateMyGoCommand, validateMyGoPackaging } from "./mygo-build.js";
|
|
2
|
+
import { ReviewSchema, toError, validateJobConfig } from "./schemas.js";
|
|
3
|
+
import { canonicalPath, expandArgv, inside } from "./config.js";
|
|
2
4
|
import { executionEnvironment, processGroupAlive, run } from "./process.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
+
import { closestOwnedGoWorkspace } from "./go-dependencies.js";
|
|
6
|
+
import { dependencyDigest, generatedModulePaths, isGeneratedPath } from "./dependencies.js";
|
|
7
|
+
import { artifactOutputPath, prepareWorkspace, projectDirectory, sourceDigest } from "./workspace.js";
|
|
5
8
|
import { checkPrerequisites } from "./preflight.js";
|
|
9
|
+
import { prepareIsolatedCommand } from "./isolation.js";
|
|
6
10
|
import { flutterEnvironment, prepareFlutter } from "./flutter.js";
|
|
7
|
-
import { prepareExportOptions } from "./export-options.js";
|
|
8
|
-
import {
|
|
11
|
+
import { prepareExportOptions, prepareNativeExportCommands } from "./export-options.js";
|
|
12
|
+
import { parseResearch, researchPrompt, reviewInstructions } from "./policy.js";
|
|
13
|
+
import { assertProject, buildInstructions } from "./project-profiles.js";
|
|
14
|
+
import { validateMacOSArtifact } from "./macos.js";
|
|
15
|
+
import { invalidateVerification } from "./proof.js";
|
|
16
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
17
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
9
19
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import { delimiter, join, relative } from "node:path";
|
|
11
|
-
import { constants, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
12
20
|
// src/runner.ts
|
|
13
|
-
const REQUIRED_POLICY_RULES = [
|
|
14
|
-
"2.1",
|
|
15
|
-
"2.3.1",
|
|
16
|
-
"2.5.19",
|
|
17
|
-
"4.2",
|
|
18
|
-
"4.2.6",
|
|
19
|
-
"4.3",
|
|
20
|
-
"5.1.1"
|
|
21
|
-
];
|
|
22
|
-
function sourceDigest(root, omitGenerated = true) {
|
|
23
|
-
if (lstatSync(root).isSymbolicLink()) throw new Error("A source or screenshot root cannot be a symlink");
|
|
24
|
-
if (omitGenerated) {
|
|
25
|
-
for (const path of GENERATED_PATHS) inside(root, path);
|
|
26
|
-
validateLocalDependencies(root);
|
|
27
|
-
}
|
|
28
|
-
const hash = createHash("sha256");
|
|
29
|
-
function visit(directory) {
|
|
30
|
-
for (const entry of readdirSync(directory).sort()) {
|
|
31
|
-
const path = join(directory, entry);
|
|
32
|
-
if (omitGenerated && isGeneratedPath(relative(root, path))) continue;
|
|
33
|
-
const stat = lstatSync(path);
|
|
34
|
-
if (stat.isSymbolicLink()) throw new Error(`Source symlink is not permitted: ${relative(root, path)}`);
|
|
35
|
-
if (stat.isDirectory()) visit(path);
|
|
36
|
-
else if (stat.isFile()) hash.update(relative(root, path)).update("\0").update(`${stat.size}:${stat.mode & 511}\0`).update(readFileSync(path));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
visit(root);
|
|
40
|
-
return hash.digest("hex");
|
|
41
|
-
}
|
|
42
21
|
function requireDependencyProof(job) {
|
|
43
22
|
if (!job.verifiedDependencyDigest) throw Object.assign(/* @__PURE__ */ new Error("Dependency resolution proof is missing; retry this stopped task to re-run verification"), { code: "SETUP_REQUIRED" });
|
|
44
|
-
if (dependencyDigest(job.workspace) !== job.verifiedDependencyDigest) throw Object.assign(/* @__PURE__ */ new Error("Dependency resolution changed since verification; re-verification is required"), { code: "EVIDENCE_CHANGED" });
|
|
45
|
-
}
|
|
46
|
-
function parseVerdict(text) {
|
|
47
|
-
const content = text.trim().replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```$/, "");
|
|
48
|
-
try {
|
|
49
|
-
return JSON.parse(content);
|
|
50
|
-
} catch {
|
|
51
|
-
throw new Error("Host did not return a JSON verdict; no approval was inferred from text or an exit code");
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
function parseResearch(text) {
|
|
55
|
-
const research = ResearchSchema.parse(parseVerdict(text));
|
|
56
|
-
const cited = /* @__PURE__ */ new Set();
|
|
57
|
-
for (const source of Array.isArray(research?.policySources) ? research.policySources : []) {
|
|
58
|
-
if (typeof source?.url !== "string") continue;
|
|
59
|
-
try {
|
|
60
|
-
const url = new URL(source.url);
|
|
61
|
-
if (url.protocol === "https:" && url.hostname === "developer.apple.com") cited.add(source.rule);
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
if (typeof research?.viable !== "boolean" || !REQUIRED_POLICY_RULES.every((rule) => cited.has(rule)) || typeof research.distinctness !== "string" || !research.distinctness.trim() || !Array.isArray(research.plan) || !research.plan.length || research.plan.some((step) => typeof step !== "string" || !step.trim())) throw new Error("Research verdict requires boolean viability, all required official policy references, distinctness reasoning and a concrete implementation plan");
|
|
65
|
-
return research;
|
|
66
|
-
}
|
|
67
|
-
function prepareWorkspace(store, job) {
|
|
68
|
-
if (!job.baseJobId || job.basePrepared) return;
|
|
69
|
-
const base = store.get(job.baseJobId);
|
|
70
|
-
if (!job.baseDigest || sourceDigest(base.workspace) !== job.baseDigest) throw Object.assign(/* @__PURE__ */ new Error("Predecessor source changed since intake; refusing to copy unverified code"), { code: "SETUP_REQUIRED" });
|
|
71
|
-
if (existsSync(job.workspace)) {
|
|
72
|
-
if (sourceDigest(job.workspace) !== job.baseDigest) throw Object.assign(/* @__PURE__ */ new Error("Uninitialized successor workspace differs from its verified predecessor; inspect it before retrying"), { code: "SETUP_REQUIRED" });
|
|
73
|
-
} else {
|
|
74
|
-
mkdirSync(job.config.workspaceRoot, {
|
|
75
|
-
recursive: true,
|
|
76
|
-
mode: 448
|
|
77
|
-
});
|
|
78
|
-
const staging = mkdtempSync(join(job.config.workspaceRoot, `.${job.id}.base-`));
|
|
79
|
-
const prepared = join(staging, "source");
|
|
80
|
-
try {
|
|
81
|
-
cpSync(base.workspace, prepared, {
|
|
82
|
-
recursive: true,
|
|
83
|
-
force: false,
|
|
84
|
-
errorOnExist: true,
|
|
85
|
-
mode: constants.COPYFILE_FICLONE,
|
|
86
|
-
filter: (path) => !isGeneratedPath(relative(base.workspace, path))
|
|
87
|
-
});
|
|
88
|
-
if (sourceDigest(prepared) !== job.baseDigest || sourceDigest(base.workspace) !== job.baseDigest) throw new Error("Predecessor changed while copying; no successor was initialized");
|
|
89
|
-
renameSync(prepared, job.workspace);
|
|
90
|
-
} finally {
|
|
91
|
-
rmSync(staging, {
|
|
92
|
-
recursive: true,
|
|
93
|
-
force: true
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
store.update(job.id, { basePrepared: true });
|
|
23
|
+
if (dependencyDigest(job.workspace, job.spec.framework, job.spec.project?.directory) !== job.verifiedDependencyDigest) throw Object.assign(/* @__PURE__ */ new Error("Dependency resolution changed since verification; re-verification is required"), { code: "EVIDENCE_CHANGED" });
|
|
98
24
|
}
|
|
99
25
|
function taskPrompt(job) {
|
|
100
|
-
return `You are the native host of an embedded
|
|
101
|
-
}
|
|
102
|
-
function assertTurnPermission(job, options) {
|
|
103
|
-
if (options.buildOnly && (job.config.allowSubmit || job.config.allowRelease)) throw new Error("--build-only refuses this task with snapshotted publication permission");
|
|
26
|
+
return `You are the native host of an embedded multi-stack app Harness. Use your own context, tools and native delegation in ${projectDirectory(job)}; never launch another coding-agent CLI. Preserve the requested framework and existing architecture. Implement only this product. Never hide functionality from users or reviewers. Do not upload, submit, push, read publisher credentials or change factory state. Remote build commands belong only to the explicitly authorized Harness packaging profile, not host turns. The workspace is not a security sandbox. Complete real implementations, not placeholders.\nImmutable product requirements:\n${JSON.stringify(job.spec, null, 2)}\nPrevious verification/review failure (repair the mechanism, never weaken acceptance):\n${job.feedback ?? job.error ?? "none"}\nPersisted execution logs:\n${JSON.stringify(job.logs.slice(-3))}\nContinue existing work. Only actual deterministic checks performed by the Harness establish verification; host claims do not.\n`;
|
|
104
27
|
}
|
|
105
28
|
function contextDigest(job) {
|
|
106
29
|
return createHash("sha256").update(JSON.stringify({
|
|
@@ -123,9 +46,9 @@ function deadlineExpired(job) {
|
|
|
123
46
|
}
|
|
124
47
|
function requireReviewProof(job) {
|
|
125
48
|
try {
|
|
126
|
-
if (!job.verifiedDigest || sourceDigest(job.workspace) !== job.verifiedDigest) throw new Error("Source changed since verification; stale evidence cannot approve a
|
|
49
|
+
if (!job.verifiedDigest || sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory) !== job.verifiedDigest) throw new Error("Source changed since verification; stale evidence cannot approve a handoff");
|
|
127
50
|
requireDependencyProof(job);
|
|
128
|
-
if (job.spec.
|
|
51
|
+
if (job.spec.screenshotsDir && sourceDigest(inside(projectDirectory(job), job.spec.screenshotsDir), false) !== job.screenshotDigest) throw new Error("Screenshots changed since runtime verification; stale evidence cannot approve a handoff");
|
|
129
52
|
} catch (cause) {
|
|
130
53
|
const error = toError(cause);
|
|
131
54
|
if (error.code !== "SETUP_REQUIRED") error.code = "EVIDENCE_CHANGED";
|
|
@@ -136,17 +59,27 @@ function hostAction(store, job) {
|
|
|
136
59
|
const turn = job.turn;
|
|
137
60
|
let instruction;
|
|
138
61
|
if (turn.stage === "research") {
|
|
139
|
-
const portfolio = store.list().filter((value) => value.id !== job.id).map((value) => ({
|
|
62
|
+
const portfolio = store.list().filter((value) => value.id !== job.id && value.spec.channel === job.spec.channel && value.spec.id !== job.spec.id).map((value) => ({
|
|
140
63
|
id: value.spec.id,
|
|
141
64
|
purpose: value.spec.purpose,
|
|
142
65
|
audience: value.spec.audience,
|
|
143
66
|
differentiators: value.spec.differentiators
|
|
144
67
|
}));
|
|
145
|
-
instruction =
|
|
146
|
-
} else if (turn.stage === "build") instruction = `Research plan: ${JSON.stringify(job.research)}\
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
68
|
+
instruction = researchPrompt(job, portfolio);
|
|
69
|
+
} else if (turn.stage === "build") instruction = `Research plan: ${JSON.stringify(job.research)}\n${buildInstructions(job.spec)} Implement the entire requested product, accessibility and realistic empty/error states. Add genuine device/simulator acceptance workflows and real screenshots when requested; never fake screenshots or bypass tests. Do not add unrequested offline/backup/account features. Use the returned environment with your tools. Frozen verification and packaging: ${JSON.stringify({
|
|
70
|
+
verification: job.config.verification,
|
|
71
|
+
packaging: job.config.packaging,
|
|
72
|
+
packageTarget: job.config.packageTarget
|
|
73
|
+
})}. Complete this turn without a result payload when source is ready; the Harness will then execute verification, not infer success.`;
|
|
74
|
+
else instruction = reviewInstructions(job);
|
|
75
|
+
const environment = job.spec.framework === "flutter" ? flutterEnvironment(job.config, job.workspace, { CI: "1" }) : { CI: "1" };
|
|
76
|
+
if (job.spec.framework === "mygo") Object.assign(environment, {
|
|
77
|
+
GOENV: "off",
|
|
78
|
+
GOTOOLCHAIN: "local",
|
|
79
|
+
GOFLAGS: "-mod=readonly",
|
|
80
|
+
GOWORK: closestOwnedGoWorkspace(job.workspace, job.spec.project?.directory) ?? "off"
|
|
81
|
+
});
|
|
82
|
+
if (job.spec.framework === "flutter" && job.config.toolchain) {
|
|
150
83
|
environment.FLUTTER_ROOT = job.config.toolchain.flutterSdk;
|
|
151
84
|
environment.PATH = `${join(job.config.toolchain.flutterSdk, "bin")}${process.env.PATH ? `${delimiter}${process.env.PATH}` : ""}`;
|
|
152
85
|
}
|
|
@@ -157,7 +90,7 @@ function hostAction(store, job) {
|
|
|
157
90
|
id: turn.id,
|
|
158
91
|
stage: turn.stage,
|
|
159
92
|
prompt: taskPrompt(job) + instruction,
|
|
160
|
-
cwd: job
|
|
93
|
+
cwd: projectDirectory(job),
|
|
161
94
|
environment: Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== void 0))
|
|
162
95
|
}
|
|
163
96
|
};
|
|
@@ -166,15 +99,8 @@ function repair(job, message) {
|
|
|
166
99
|
return {
|
|
167
100
|
stage: "build",
|
|
168
101
|
status: (job.attempts[job.stage] ?? 0) >= job.config.maxAttempts ? "failed" : "waiting",
|
|
102
|
+
...invalidateVerification(),
|
|
169
103
|
turn: null,
|
|
170
|
-
review: null,
|
|
171
|
-
verifiedDigest: null,
|
|
172
|
-
verifiedDependencyDigest: null,
|
|
173
|
-
screenshotDigest: null,
|
|
174
|
-
verifiedAt: void 0,
|
|
175
|
-
artifact: null,
|
|
176
|
-
ipaPath: null,
|
|
177
|
-
archiveDigest: null,
|
|
178
104
|
feedback: message,
|
|
179
105
|
error: message,
|
|
180
106
|
nextAt: Date.now() + job.config.retryDelayMs
|
|
@@ -184,13 +110,11 @@ async function nextTurn(store, id, options = {}) {
|
|
|
184
110
|
store.acquire();
|
|
185
111
|
try {
|
|
186
112
|
let job = store.get(id);
|
|
187
|
-
assertTurnPermission(job, options);
|
|
188
113
|
if (job.turn) {
|
|
189
114
|
if (job.status !== "running" || job.stage !== job.turn.stage || contextDigest(job) !== job.turn.contextDigest) throw new Error("Active host turn context changed; abandon its token before requesting another turn");
|
|
190
115
|
if (deadlineExpired(job)) {
|
|
191
116
|
const claimed = job.turn;
|
|
192
117
|
job = store.transition(id, (current) => {
|
|
193
|
-
assertTurnPermission(current, options);
|
|
194
118
|
if (current.turn?.id !== claimed.id || contextDigest(current) !== claimed.contextDigest) throw new Error("Host turn changed while checking its deadline; inspect the current task");
|
|
195
119
|
return {
|
|
196
120
|
turn: null,
|
|
@@ -212,9 +136,7 @@ async function nextTurn(store, id, options = {}) {
|
|
|
212
136
|
if ([
|
|
213
137
|
"failed",
|
|
214
138
|
"ready",
|
|
215
|
-
"built"
|
|
216
|
-
"approved",
|
|
217
|
-
"released"
|
|
139
|
+
"built"
|
|
218
140
|
].includes(job.status)) return {
|
|
219
141
|
job,
|
|
220
142
|
action: "done"
|
|
@@ -233,7 +155,6 @@ async function nextTurn(store, id, options = {}) {
|
|
|
233
155
|
};
|
|
234
156
|
if (deadlineExpired(job) || job.hostTurns >= job.config.maxHostTurns || (job.attempts[job.stage] ?? 0) >= job.config.maxAttempts) {
|
|
235
157
|
job = store.transition(id, (current) => {
|
|
236
|
-
assertTurnPermission(current, options);
|
|
237
158
|
return {
|
|
238
159
|
status: "failed",
|
|
239
160
|
error: "Host turn, stage attempt or task wall-clock limit reached; no further host work will start"
|
|
@@ -247,7 +168,7 @@ async function nextTurn(store, id, options = {}) {
|
|
|
247
168
|
try {
|
|
248
169
|
if (job.stage === "review") requireReviewProof(job);
|
|
249
170
|
prepareWorkspace(store, job);
|
|
250
|
-
mkdirSync(job
|
|
171
|
+
mkdirSync(projectDirectory(job), {
|
|
251
172
|
recursive: true,
|
|
252
173
|
mode: 448
|
|
253
174
|
});
|
|
@@ -263,7 +184,6 @@ async function nextTurn(store, id, options = {}) {
|
|
|
263
184
|
};
|
|
264
185
|
}
|
|
265
186
|
job = store.transition(id, (current) => {
|
|
266
|
-
assertTurnPermission(current, options);
|
|
267
187
|
const eligible = ["queued", "waiting"].includes(current.status) || current.status === "blocked" && current.blockedReason?.kind === "prerequisite";
|
|
268
188
|
if (current.turn || contextDigest(current) !== contextDigest(job) || current.nextAt > Date.now() || !eligible || current.groups.length || current.manualRecovery) throw new Error("Task changed before the host turn could be claimed");
|
|
269
189
|
if (deadlineExpired(current) || current.hostTurns >= current.config.maxHostTurns || (current.attempts[current.stage] ?? 0) >= current.config.maxAttempts) return {
|
|
@@ -298,7 +218,6 @@ async function nextTurn(store, id, options = {}) {
|
|
|
298
218
|
}
|
|
299
219
|
function completeTurn(store, id, turnId, result, options = {}) {
|
|
300
220
|
return store.transition(id, (job) => {
|
|
301
|
-
assertTurnPermission(job, options);
|
|
302
221
|
if (!job.turn || job.turn.id !== turnId || job.turn.stage !== job.stage || job.status !== "running") throw new Error("Stale, replayed or wrong-phase host turn; use next to inspect the active token");
|
|
303
222
|
if (contextDigest(job) !== job.turn.contextDigest) throw new Error("Host turn context changed; abandon this stale token before continuing");
|
|
304
223
|
if (job.groups.length || job.manualRecovery) throw new Error("Command recovery must finish before completing a host turn");
|
|
@@ -309,29 +228,21 @@ function completeTurn(store, id, turnId, result, options = {}) {
|
|
|
309
228
|
};
|
|
310
229
|
let patch;
|
|
311
230
|
if (job.stage === "research") {
|
|
312
|
-
const research = parseResearch(JSON.stringify(
|
|
231
|
+
const research = parseResearch(JSON.stringify(result), job.spec.channel);
|
|
313
232
|
patch = research.viable ? {
|
|
314
233
|
stage: "build",
|
|
315
234
|
research
|
|
316
235
|
} : {
|
|
317
236
|
status: "failed",
|
|
318
237
|
research,
|
|
319
|
-
error: "Research rejected this product; no replacement product
|
|
238
|
+
error: "Research rejected this product; no replacement product will be attempted"
|
|
320
239
|
};
|
|
321
240
|
} else if (job.stage === "build") {
|
|
322
241
|
if (result !== void 0) throw new Error("Build completion accepts no result payload; actual deterministic verification is required");
|
|
323
|
-
|
|
324
|
-
if (!lstatSync(project).isFile()) throw new Error("Build completion requires a regular pubspec.yaml in the task workspace");
|
|
242
|
+
assertProject(projectDirectory(job), job.spec);
|
|
325
243
|
patch = {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
verifiedDependencyDigest: null,
|
|
329
|
-
screenshotDigest: null,
|
|
330
|
-
verifiedAt: void 0,
|
|
331
|
-
review: null,
|
|
332
|
-
artifact: null,
|
|
333
|
-
ipaPath: null,
|
|
334
|
-
archiveDigest: null
|
|
244
|
+
...invalidateVerification(),
|
|
245
|
+
stage: job.config.obfuscation ? "obfuscate" : "verify"
|
|
335
246
|
};
|
|
336
247
|
} else {
|
|
337
248
|
const review = ReviewSchema.parse(result);
|
|
@@ -365,7 +276,6 @@ function completeTurn(store, id, turnId, result, options = {}) {
|
|
|
365
276
|
}
|
|
366
277
|
function abandonTurn(store, id, turnId, options = {}) {
|
|
367
278
|
return store.transition(id, (job) => {
|
|
368
|
-
assertTurnPermission(job, options);
|
|
369
279
|
if (!job.turn || job.turn.id !== turnId) throw new Error("Stale or replayed host turn; no active claim was abandoned");
|
|
370
280
|
if (job.groups.length || job.manualRecovery) throw new Error("Command recovery must finish before abandoning a host turn");
|
|
371
281
|
store.event(id, {
|
|
@@ -406,30 +316,46 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
406
316
|
status: "blocked",
|
|
407
317
|
error: config.executionWarning ?? "Local execution is disabled. Explicitly enable allowLocalExecution only for trusted deterministic commands in a dedicated account or VM."
|
|
408
318
|
});
|
|
409
|
-
if (deadlineExpired(job)
|
|
319
|
+
if (deadlineExpired(job)) return store.update(job.id, {
|
|
410
320
|
status: "failed",
|
|
411
321
|
error: "Task wall-clock limit reached; no further work will start"
|
|
412
322
|
});
|
|
413
323
|
const attemptNumber = (job.attempts[job.stage] ?? 0) + 1;
|
|
414
324
|
let attempts = { ...job.attempts };
|
|
415
325
|
let attemptCharged = false;
|
|
416
|
-
const developerEnv = flutterEnvironment(config, job.workspace, executionEnvironment(config));
|
|
417
|
-
const verifierEnv = flutterEnvironment(config, job.workspace, executionEnvironment(config), true);
|
|
326
|
+
const developerEnv = job.spec.framework === "flutter" ? flutterEnvironment(config, job.workspace, executionEnvironment(config)) : executionEnvironment(config);
|
|
327
|
+
const verifierEnv = job.spec.framework === "flutter" ? flutterEnvironment(config, job.workspace, executionEnvironment(config), true) : developerEnv;
|
|
418
328
|
const environment = ["verify", "package"].includes(job.stage) ? verifierEnv : developerEnv;
|
|
419
329
|
const logPath = join(config.stateDir, "logs", job.id, `${job.stage}-${attemptNumber}-${Date.now()}.log`);
|
|
420
330
|
store.update(job.id, { logs: [...job.logs, logPath] });
|
|
421
|
-
const
|
|
331
|
+
const readableFiles = /* @__PURE__ */ new Set();
|
|
332
|
+
const trackedRun = async (argv, options = {}, capabilities = {}) => {
|
|
422
333
|
let pid;
|
|
334
|
+
let isolated;
|
|
423
335
|
try {
|
|
424
336
|
const effectiveEnv = options.env ?? environment;
|
|
337
|
+
if (job.spec.framework === "mygo") validateMyGoCommand(config, argv);
|
|
425
338
|
const command = effectiveEnv.FLUTTER_ROOT && ["flutter", "dart"].includes(argv[0]) ? [join(effectiveEnv.FLUTTER_ROOT, "bin", argv[0]), ...argv.slice(1)] : argv;
|
|
426
|
-
const
|
|
427
|
-
|
|
339
|
+
const cwd = options.cwd ?? projectDirectory(job);
|
|
340
|
+
if (config.isolation && [
|
|
341
|
+
"obfuscate",
|
|
342
|
+
"verify",
|
|
343
|
+
"package"
|
|
344
|
+
].includes(job.stage)) isolated = prepareIsolatedCommand(command, {
|
|
345
|
+
config,
|
|
346
|
+
workspace: job.workspace,
|
|
347
|
+
cwd,
|
|
428
348
|
env: effectiveEnv,
|
|
349
|
+
readableFiles: [...readableFiles, ...capabilities.readableFiles ?? []],
|
|
350
|
+
writableDirectories: capabilities.writableDirectories
|
|
351
|
+
});
|
|
352
|
+
const result = await run(isolated?.argv ?? command, {
|
|
429
353
|
timeoutMs: config.timeoutMs,
|
|
430
354
|
signal,
|
|
431
355
|
logPath,
|
|
432
356
|
...options,
|
|
357
|
+
cwd,
|
|
358
|
+
env: isolated?.env ?? effectiveEnv,
|
|
433
359
|
onSpawn: (value, metadata) => {
|
|
434
360
|
pid = value;
|
|
435
361
|
const current = store.get(job.id);
|
|
@@ -452,33 +378,93 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
452
378
|
}
|
|
453
379
|
return result;
|
|
454
380
|
} finally {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
381
|
+
try {
|
|
382
|
+
const current = store.get(job.id);
|
|
383
|
+
const groups = current.groups.filter(processGroupAlive);
|
|
384
|
+
const commands = Object.fromEntries(Object.entries(current.commands ?? {}).filter(([pid]) => groups.includes(Number(pid))));
|
|
385
|
+
store.update(job.id, {
|
|
386
|
+
groups,
|
|
387
|
+
commands,
|
|
388
|
+
recoveryStage: groups.length ? current.recoveryStage ?? job.stage : void 0
|
|
389
|
+
});
|
|
390
|
+
} finally {
|
|
391
|
+
isolated?.cleanup();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const exec = (argv, options) => trackedRun(argv, options);
|
|
396
|
+
const utilityExec = (argv, options) => {
|
|
397
|
+
if (!config.isolation) return trackedRun(argv, options);
|
|
398
|
+
if (argv[0] !== "/usr/bin/plutil") throw Object.assign(/* @__PURE__ */ new Error("Unexpected isolated export utility"), { code: "SETUP_REQUIRED" });
|
|
399
|
+
const file = resolve(options?.cwd ?? projectDirectory(job), argv[argv.length - 1]);
|
|
400
|
+
const actual = canonicalPath(file), state = canonicalPath(config.stateDir), directory = dirname(actual);
|
|
401
|
+
const within = (root, target) => {
|
|
402
|
+
const path = relative(root, target);
|
|
403
|
+
return path === "" || path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
404
|
+
};
|
|
405
|
+
if (within(job.workspace, file) && !within(job.workspace, actual) && !config.isolation.readOnlyPaths.some((root) => within(root, actual))) throw Object.assign(/* @__PURE__ */ new Error("Workspace export options cannot grant access to an unapproved external file"), { code: "SETUP_REQUIRED" });
|
|
406
|
+
const snapshot = dirname(directory) === state && basename(directory).startsWith("export-options-") && basename(actual) === "ExportOptions.plist";
|
|
407
|
+
if (within(state, actual) && !snapshot || within(canonicalPath(config.workspaceRoot), actual) && !within(job.workspace, actual) || config.configPath && actual === canonicalPath(config.configPath)) throw Object.assign(/* @__PURE__ */ new Error("Export utility cannot read protected control state or another task"), { code: "SETUP_REQUIRED" });
|
|
408
|
+
const writing = argv[1] === "-convert" && argv[2] === "xml1";
|
|
409
|
+
if (writing && !snapshot) throw Object.assign(/* @__PURE__ */ new Error("Export utility writes require a harness-owned snapshot"), { code: "SETUP_REQUIRED" });
|
|
410
|
+
return trackedRun([...argv.slice(0, -1), actual], options, {
|
|
411
|
+
readableFiles: within(job.workspace, actual) ? [] : [actual],
|
|
412
|
+
writableDirectories: writing ? [directory] : []
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const removeArtifact = async (output) => {
|
|
416
|
+
if (!config.isolation) {
|
|
417
|
+
rmSync(output, {
|
|
418
|
+
recursive: true,
|
|
419
|
+
force: true
|
|
462
420
|
});
|
|
421
|
+
return;
|
|
463
422
|
}
|
|
423
|
+
const result = await exec([
|
|
424
|
+
"/bin/rm",
|
|
425
|
+
"-rf",
|
|
426
|
+
"--",
|
|
427
|
+
output
|
|
428
|
+
]);
|
|
429
|
+
if (result.code !== 0) throw Object.assign(/* @__PURE__ */ new Error(`Isolated artifact cleanup was denied: ${result.stderr || result.stdout}`), { code: "SETUP_REQUIRED" });
|
|
464
430
|
};
|
|
465
431
|
try {
|
|
432
|
+
validateJobConfig(job.spec, config);
|
|
433
|
+
if (job.spec.framework === "mygo") validateMyGoPackaging(config);
|
|
466
434
|
prepareWorkspace(store, job);
|
|
467
|
-
mkdirSync(job
|
|
435
|
+
mkdirSync(projectDirectory(job), {
|
|
468
436
|
recursive: true,
|
|
469
437
|
mode: 448
|
|
470
438
|
});
|
|
439
|
+
if (job.spec.framework === "mygo") {
|
|
440
|
+
Object.assign(developerEnv, {
|
|
441
|
+
GOENV: "off",
|
|
442
|
+
GOTOOLCHAIN: "local",
|
|
443
|
+
GOFLAGS: "-mod=readonly",
|
|
444
|
+
GOWORK: closestOwnedGoWorkspace(job.workspace, job.spec.project?.directory) ?? "off",
|
|
445
|
+
GOCACHE: developerEnv.GOCACHE ?? join(config.stateDir, "verification-cache", "go-build"),
|
|
446
|
+
GOMODCACHE: developerEnv.GOMODCACHE ?? join(config.stateDir, "verification-cache", "go-mod")
|
|
447
|
+
});
|
|
448
|
+
for (const key of [
|
|
449
|
+
"MYGO_UPDATER_PRIVATE_KEY",
|
|
450
|
+
"MYGO_WINDOWS_CERTIFICATE_PASSWORD",
|
|
451
|
+
"MYGO_CLI_BINARY",
|
|
452
|
+
"MYGO_LDFLAGS",
|
|
453
|
+
"MYGO_GENERATE",
|
|
454
|
+
"MYGO_INSPECTOR"
|
|
455
|
+
]) delete developerEnv[key];
|
|
456
|
+
}
|
|
471
457
|
if (job.stage === "verify" || job.stage === "package") await checkPrerequisites(config.verification.prerequisites ?? [], {
|
|
472
|
-
cwd: job
|
|
458
|
+
cwd: projectDirectory(job),
|
|
473
459
|
env: environment,
|
|
474
460
|
run: exec,
|
|
475
461
|
signal
|
|
476
462
|
});
|
|
477
|
-
if ((job.attempts[job.stage] ?? 0) >= config.maxAttempts
|
|
463
|
+
if ((job.attempts[job.stage] ?? 0) >= config.maxAttempts) return store.update(job.id, {
|
|
478
464
|
status: "failed",
|
|
479
465
|
error: `Attempt limit reached at ${job.stage}`
|
|
480
466
|
});
|
|
481
|
-
if (config.toolchain && ["verify", "package"].includes(job.stage)) {
|
|
467
|
+
if (job.spec.framework === "flutter" && config.toolchain && ["verify", "package"].includes(job.stage)) {
|
|
482
468
|
const toolchain = await prepareFlutter(job, {
|
|
483
469
|
run: exec,
|
|
484
470
|
env: developerEnv,
|
|
@@ -493,7 +479,7 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
493
479
|
store.update(job.id, { attempts });
|
|
494
480
|
attemptCharged = true;
|
|
495
481
|
if (job.stage === "verify") {
|
|
496
|
-
if (config.toolchain) {
|
|
482
|
+
if (job.spec.framework === "flutter" && config.toolchain) {
|
|
497
483
|
const dependencies = await exec([
|
|
498
484
|
"flutter",
|
|
499
485
|
"pub",
|
|
@@ -501,147 +487,163 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
501
487
|
]);
|
|
502
488
|
if (dependencies.code !== 0) throw new Error(`Verification dependency resolution failed: ${dependencies.stderr.slice(-4e3)}`);
|
|
503
489
|
}
|
|
504
|
-
const before = sourceDigest(job.workspace);
|
|
505
|
-
const dependenciesBefore = dependencyDigest(job.workspace);
|
|
490
|
+
const before = sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory);
|
|
491
|
+
const dependenciesBefore = dependencyDigest(job.workspace, job.spec.framework, job.spec.project?.directory);
|
|
506
492
|
for (const argv of [...config.verification.checks, ...config.verification.runtime]) {
|
|
507
493
|
const result = await exec(expandArgv(argv, executionEnvironment(config)));
|
|
508
494
|
if (result.code !== 0) throw new Error(`Verification failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
|
|
509
495
|
}
|
|
510
|
-
if (sourceDigest(job.workspace) !== before) throw new Error("Verification changed source files; the new revision must be verified again");
|
|
511
|
-
if (dependencyDigest(job.workspace) !== dependenciesBefore) throw new Error("Verification changed dependency resolution; the new resolution must be verified again");
|
|
496
|
+
if (sourceDigest(job.workspace, true, job.spec.framework, job.spec.project?.directory) !== before) throw new Error("Verification changed source files; the new revision must be verified again");
|
|
497
|
+
if (dependencyDigest(job.workspace, job.spec.framework, job.spec.project?.directory) !== dependenciesBefore) throw new Error("Verification changed dependency resolution; the new resolution must be verified again");
|
|
512
498
|
return next(store, job, "review", {
|
|
513
499
|
verifiedDigest: before,
|
|
514
500
|
verifiedDependencyDigest: dependenciesBefore,
|
|
515
|
-
screenshotDigest: job.spec.
|
|
501
|
+
screenshotDigest: job.spec.screenshotsDir ? sourceDigest(inside(projectDirectory(job), job.spec.screenshotsDir), false) : null,
|
|
516
502
|
verifiedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
517
503
|
});
|
|
518
504
|
}
|
|
505
|
+
if (job.stage === "obfuscate") {
|
|
506
|
+
const obfuscation = job.config.obfuscation;
|
|
507
|
+
if (!obfuscation) throw Object.assign(/* @__PURE__ */ new Error("Obfuscation stage requires config.obfuscation"), { code: "SETUP_REQUIRED" });
|
|
508
|
+
const expanded = expandArgv(obfuscation.command, environment);
|
|
509
|
+
const bundled = fileURLToPath(new URL("./vendor/obfuscator/dist/cli.js", import.meta.url));
|
|
510
|
+
const argv = [
|
|
511
|
+
...expanded[0] === "obfuscator" && existsSync(bundled) ? [
|
|
512
|
+
process.execPath,
|
|
513
|
+
bundled,
|
|
514
|
+
...expanded.slice(1)
|
|
515
|
+
] : expanded,
|
|
516
|
+
"--project",
|
|
517
|
+
projectDirectory(job),
|
|
518
|
+
"--config",
|
|
519
|
+
obfuscation.configPath,
|
|
520
|
+
"--framework",
|
|
521
|
+
job.spec.framework,
|
|
522
|
+
...obfuscation.seed ? ["--seed", obfuscation.seed] : []
|
|
523
|
+
];
|
|
524
|
+
const result = await trackedRun(argv, {}, { readableFiles: [obfuscation.configPath] });
|
|
525
|
+
if (result.code !== 0) throw new Error(`Obfuscation failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
|
|
526
|
+
return next(store, job, "verify");
|
|
527
|
+
}
|
|
519
528
|
if (job.stage === "package") {
|
|
520
529
|
requireReviewProof(job);
|
|
521
530
|
if (!job.review?.approved || job.review.findings.length) throw new Error("Packaging requires an approved host review");
|
|
522
|
-
if (process.platform !== "darwin") throw Object.assign(/* @__PURE__ */ new Error("iOS packaging requires the Mac worker; use the SSH controller from Linux"), { code: "SETUP_REQUIRED" });
|
|
523
531
|
const target = config.packageTarget;
|
|
524
|
-
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
532
|
+
const project = projectDirectory(job);
|
|
533
|
+
const profile = config.packaging;
|
|
534
|
+
if (job.cloudBuildPending) throw Object.assign(/* @__PURE__ */ new Error("A previous cloud build may have remote effects. Reconcile it with the provider before creating a new task; this build will not be replayed."), { code: "SETUP_REQUIRED" });
|
|
535
|
+
if (["ios", "macos"].includes(job.spec.platform) && profile?.provider !== "cloud" && process.platform !== "darwin") throw Object.assign(/* @__PURE__ */ new Error(`Local ${job.spec.platform} packaging requires a macOS worker`), { code: "SETUP_REQUIRED" });
|
|
536
|
+
let path;
|
|
537
|
+
store.update(job.id, {
|
|
538
|
+
artifact: null,
|
|
539
|
+
ipaPath: null,
|
|
540
|
+
archiveDigest: null
|
|
541
|
+
});
|
|
542
|
+
if (profile) {
|
|
543
|
+
const output = artifactOutputPath(project, profile.artifactPath);
|
|
544
|
+
const outputRelative = relative(job.workspace, output).split(sep).join("/");
|
|
545
|
+
let generated = isGeneratedPath(profile.artifactPath, job.spec.framework);
|
|
546
|
+
if (!generated) {
|
|
547
|
+
for (const module of generatedModulePaths(job.workspace, job.spec.framework, job.spec.project?.directory)) if (outputRelative.startsWith(`${module}/`)) {
|
|
548
|
+
generated = true;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (!generated || !/(?:^|\/)(?:build|dist|unpackage)(?:\/|$)/.test(profile.artifactPath) || /(?:^|\/)(?:node_modules|\.git|\.gradle|\.factory-cache|Pods)(?:\/|$)/.test(profile.artifactPath)) throw Object.assign(/* @__PURE__ */ new Error("packaging.artifactPath must name an exact generated build/dist/unpackage artifact, never source or dependency caches"), { code: "SETUP_REQUIRED" });
|
|
553
|
+
if (!output.endsWith(target === "simulator" || target === "app" ? ".app" : `.${target}`)) throw Object.assign(/* @__PURE__ */ new Error(`packaging.artifactPath must end in ${target === "simulator" || target === "app" ? ".app" : `.${target}`}`), { code: "SETUP_REQUIRED" });
|
|
554
|
+
const prepared = await prepareNativeExportCommands(profile.commands.map((argv) => expandArgv(argv, executionEnvironment(config))), project, config.stateDir, utilityExec);
|
|
555
|
+
try {
|
|
556
|
+
for (const file of prepared.readableFiles) readableFiles.add(file);
|
|
557
|
+
await removeArtifact(output);
|
|
558
|
+
if (profile.provider === "cloud") store.update(job.id, { cloudBuildPending: true });
|
|
559
|
+
for (const argv of prepared.commands) {
|
|
560
|
+
const result = await exec(argv);
|
|
561
|
+
if (result.code !== 0) throw new Error(`Packaging failed (${result.code}) ${JSON.stringify(argv)}:\n${result.stderr.slice(-5e3)}\n${result.stdout.slice(-5e3)}`);
|
|
562
|
+
}
|
|
563
|
+
path = artifactOutputPath(project, profile.artifactPath);
|
|
564
|
+
} finally {
|
|
565
|
+
for (const file of prepared.readableFiles) readableFiles.delete(file);
|
|
566
|
+
prepared.cleanup();
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
const output = artifactOutputPath(project, target === "ipa" ? "build/ios/ipa" : target === "simulator" ? "build/ios/iphonesimulator" : target === "apk" ? "build/app/outputs/flutter-apk" : "build/app/outputs/bundle/release");
|
|
570
|
+
if (target === "ipa" && !config.apple?.exportOptionsPlist) throw Object.assign(/* @__PURE__ */ new Error("Configure apple.exportOptionsPlist and valid local signing before IPA packaging"), { code: "SETUP_REQUIRED" });
|
|
571
|
+
if (target === "ipa" && !job.spec.apple?.buildNumber) throw Object.assign(/* @__PURE__ */ new Error("apple.buildNumber is required for IPA signing"), { code: "SETUP_REQUIRED" });
|
|
572
|
+
let exportOptions;
|
|
573
|
+
try {
|
|
574
|
+
if (target === "ipa") {
|
|
575
|
+
exportOptions = await prepareExportOptions(config.apple.exportOptionsPlist, config.stateDir, utilityExec);
|
|
576
|
+
readableFiles.add(exportOptions.path);
|
|
577
|
+
}
|
|
578
|
+
await removeArtifact(output);
|
|
579
|
+
const built = await exec(target === "ipa" ? [
|
|
580
|
+
"flutter",
|
|
581
|
+
"build",
|
|
582
|
+
"ipa",
|
|
583
|
+
"--release",
|
|
584
|
+
"--build-name",
|
|
585
|
+
job.spec.version,
|
|
586
|
+
"--build-number",
|
|
587
|
+
job.spec.apple.buildNumber,
|
|
588
|
+
"--export-options-plist",
|
|
589
|
+
exportOptions.path
|
|
590
|
+
] : target === "simulator" ? [
|
|
591
|
+
"flutter",
|
|
592
|
+
"build",
|
|
593
|
+
"ios",
|
|
594
|
+
"--simulator",
|
|
595
|
+
"--debug",
|
|
596
|
+
"--no-pub"
|
|
597
|
+
] : [
|
|
598
|
+
"flutter",
|
|
599
|
+
"build",
|
|
600
|
+
target === "apk" ? "apk" : "appbundle",
|
|
601
|
+
"--release",
|
|
602
|
+
"--no-pub",
|
|
603
|
+
"--build-name",
|
|
604
|
+
job.spec.version
|
|
605
|
+
]);
|
|
606
|
+
if (built.code !== 0) throw new Error(`${job.spec.framework} ${target} build failed: ${built.stderr.slice(-5e3)}`);
|
|
607
|
+
} finally {
|
|
608
|
+
if (exportOptions) {
|
|
609
|
+
readableFiles.delete(exportOptions.path);
|
|
610
|
+
rmSync(exportOptions.directory, {
|
|
611
|
+
recursive: true,
|
|
612
|
+
force: true
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const products = readdirSync(output).filter((name) => name.endsWith(target === "simulator" ? ".app" : `.${target}`));
|
|
617
|
+
if (products.length !== 1) throw new Error(`Expected exactly one ${target} artifact in ${relative(job.workspace, output)}`);
|
|
618
|
+
path = artifactOutputPath(project, join(relative(project, output), products[0]));
|
|
566
619
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
const
|
|
573
|
-
const digest = target === "ipa" ? createHash("sha256").update(readFileSync(path)).digest("hex") : sourceDigest(path, false);
|
|
620
|
+
requireReviewProof(job);
|
|
621
|
+
const stat = lstatSync(path);
|
|
622
|
+
const bundle = target === "simulator" || target === "app";
|
|
623
|
+
if (stat.isSymbolicLink() || (bundle ? !stat.isDirectory() || readdirSync(path).length === 0 : !stat.isFile() || stat.size === 0)) throw new Error("Packaging did not produce a nonempty regular artifact of the requested kind");
|
|
624
|
+
if (target === "app" || target === "dmg") await validateMacOSArtifact(path, target, exec);
|
|
625
|
+
const digest = bundle ? sourceDigest(path, false) : createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
574
626
|
const artifact = {
|
|
575
|
-
kind: target === "
|
|
627
|
+
kind: target === "simulator" ? "ios-simulator" : target === "app" ? "macos-app" : target,
|
|
576
628
|
path,
|
|
577
629
|
digest,
|
|
578
630
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
579
631
|
};
|
|
580
|
-
|
|
632
|
+
return store.update(job.id, {
|
|
581
633
|
status: "built",
|
|
582
634
|
artifact,
|
|
583
|
-
|
|
584
|
-
});
|
|
585
|
-
return next(store, job, "publish", {
|
|
586
|
-
artifact,
|
|
587
|
-
ipaPath: path,
|
|
588
|
-
archiveDigest: digest
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
if (job.stage === "publish") {
|
|
592
|
-
if (!config.allowSubmit) throw Object.assign(/* @__PURE__ */ new Error("This task has no upload/submission permission"), { code: "SETUP_REQUIRED" });
|
|
593
|
-
if (!job.ipaPath || !job.archiveDigest) throw new Error("Publication requires a completed verified IPA artifact");
|
|
594
|
-
if (sourceDigest(job.workspace) !== job.verifiedDigest || createHash("sha256").update(readFileSync(job.ipaPath)).digest("hex") !== job.archiveDigest) throw new Error("Source or archive changed after verification; refusing publication");
|
|
595
|
-
requireDependencyProof(job);
|
|
596
|
-
const spec = structuredClone(job.spec);
|
|
597
|
-
if (spec.apple?.screenshotsDir) spec.apple.screenshotsDir = inside(job.workspace, spec.apple.screenshotsDir);
|
|
598
|
-
if (spec.apple?.screenshotsDir && sourceDigest(spec.apple.screenshotsDir, false) !== job.screenshotDigest) throw new Error("Screenshots changed after runtime verification; refusing to upload different assets");
|
|
599
|
-
return next(store, job, "monitor", {
|
|
600
|
-
receipt: await shipApple({
|
|
601
|
-
spec,
|
|
602
|
-
ipaPath: job.ipaPath,
|
|
603
|
-
state: store.get(job.id).receipt,
|
|
604
|
-
save: (receipt) => {
|
|
605
|
-
store.update(job.id, { receipt });
|
|
606
|
-
},
|
|
607
|
-
run: exec,
|
|
608
|
-
signal,
|
|
609
|
-
allowRelease: config.allowRelease
|
|
610
|
-
}),
|
|
611
|
-
status: "waiting",
|
|
612
|
-
nextAt: Date.now() + config.pollMs
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
if (job.stage === "monitor") {
|
|
616
|
-
const result = await pollApple({
|
|
617
|
-
spec: job.spec,
|
|
618
|
-
state: job.receipt,
|
|
619
|
-
signal
|
|
620
|
-
});
|
|
621
|
-
if (result.status === "approved" && (result.available || !config.allowRelease)) return store.update(job.id, {
|
|
622
|
-
status: result.available ? "released" : "approved",
|
|
623
|
-
appleReview: result,
|
|
624
|
-
error: null
|
|
625
|
-
});
|
|
626
|
-
if (result.status === "rejected") return store.update(job.id, {
|
|
627
|
-
status: "blocked",
|
|
628
|
-
appleReview: result,
|
|
629
|
-
error: "Apple rejected this version. Rejection message details are not available through this API; do not invent feedback. Supply the actual App Review message with the feedback command to resume repairs."
|
|
630
|
-
});
|
|
631
|
-
return store.update(job.id, {
|
|
632
|
-
status: "waiting",
|
|
633
|
-
appleReview: result,
|
|
634
|
-
attempts: {
|
|
635
|
-
...attempts,
|
|
636
|
-
monitor: 0
|
|
637
|
-
},
|
|
638
|
-
nextAt: Date.now() + config.pollMs,
|
|
635
|
+
cloudBuildPending: false,
|
|
639
636
|
error: null
|
|
640
637
|
});
|
|
641
638
|
}
|
|
642
639
|
throw new Error(`Unknown task stage ${job.stage}`);
|
|
643
640
|
} catch (cause) {
|
|
644
641
|
const error = toError(cause);
|
|
642
|
+
if (store.get(job.id).cloudBuildPending) return store.update(job.id, {
|
|
643
|
+
status: "blocked",
|
|
644
|
+
blockedReason: null,
|
|
645
|
+
error: `Cloud build outcome requires provider reconciliation; automatic replay is forbidden. ${error.message}`
|
|
646
|
+
});
|
|
645
647
|
if (error.code === "PROCESS_CLEANUP_BLOCKED") return store.update(job.id, {
|
|
646
648
|
status: "blocked",
|
|
647
649
|
blockedReason: null,
|
|
@@ -675,27 +677,7 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
675
677
|
nextAt: Date.now(),
|
|
676
678
|
error: error.message
|
|
677
679
|
});
|
|
678
|
-
if (error.code === "ENOENT" && error.syscall?.startsWith("spawn") || /SETUP_REQUIRED|UNSUPPORTED|
|
|
679
|
-
status: "blocked",
|
|
680
|
-
error: error.message
|
|
681
|
-
});
|
|
682
|
-
if (error.code === "APPLE_PROCESSING" || error.code === "APPLE_RATE_LIMIT") return store.update(job.id, {
|
|
683
|
-
status: "waiting",
|
|
684
|
-
attempts: {
|
|
685
|
-
...attempts,
|
|
686
|
-
[job.stage]: Math.max(0, (attempts[job.stage] ?? 0) - 1)
|
|
687
|
-
},
|
|
688
|
-
error: error.message,
|
|
689
|
-
nextAt: Date.now() + config.pollMs
|
|
690
|
-
});
|
|
691
|
-
const receipt = store.get(job.id).receipt;
|
|
692
|
-
const publicationRecovery = ["publish", "monitor"].includes(job.stage) && (job.stage === "monitor" || !!(receipt.upload || receipt.pending || receipt.buildId || receipt.submissionId || receipt.versionId || Object.keys(receipt.effects ?? {}).length));
|
|
693
|
-
if (publicationRecovery && error.transient === true) return store.update(job.id, {
|
|
694
|
-
status: "waiting",
|
|
695
|
-
error: error.message,
|
|
696
|
-
nextAt: Date.now() + config.pollMs
|
|
697
|
-
});
|
|
698
|
-
if (job.stage === "monitor" || publicationRecovery && error.code?.startsWith("APPLE_")) return store.update(job.id, {
|
|
680
|
+
if (error.code === "ENOENT" && error.syscall?.startsWith("spawn") || /SETUP_REQUIRED|UNSUPPORTED|PROCESS_CLEANUP_BLOCKED/.test(error.code ?? "")) return store.update(job.id, {
|
|
699
681
|
status: "blocked",
|
|
700
682
|
error: error.message
|
|
701
683
|
});
|
|
@@ -717,6 +699,6 @@ async function executeStep(store, job, { signal } = {}) {
|
|
|
717
699
|
});
|
|
718
700
|
}
|
|
719
701
|
}
|
|
720
|
-
export { abandonTurn, completeTurn, executeStep, nextTurn
|
|
702
|
+
export { abandonTurn, completeTurn, executeStep, nextTurn };
|
|
721
703
|
|
|
722
704
|
//# sourceMappingURL=runner.js.map
|