carthagent 1.0.7 → 1.1.0-preview.13
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 +140 -83
- package/bin/ctg.mjs +14 -732
- package/docs/glyph-review/host-chrome-glyph-compatibility-v4.md +83 -0
- package/docs/glyph-review/test-isolation-v1.md +26 -0
- package/docs/glyph-review/typecheck-base.log +1 -0
- package/docs/glyph-review/typecheck-v4.log +1 -0
- package/docs/glyph-review/unicode-inventory-v4.csv +402 -0
- package/docs/help-dispatch-review.md +69 -0
- package/docs/legacy-cli.md +150 -0
- package/docs/live-theme-plugin-api.md +18 -0
- package/docs/native-cli.md +102 -0
- package/docs/native-compatibility.md +16 -0
- package/docs/native-daily-quests.md +132 -0
- package/docs/native-glyph-pty.md +59 -0
- package/docs/native-manual-testing.md +336 -0
- package/docs/native-slash-acceptance-handoff.md +49 -0
- package/docs/native-terminal-paired-failure-2026-10-17.md +40 -0
- package/docs/native-terminal-performance.md +310 -0
- package/docs/native-terminal-semantics-evidence/README.md +25 -0
- package/docs/native-terminal-semantics-evidence/controlled-host-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/current-acceptance-status.json +59 -0
- package/docs/native-terminal-semantics-evidence/final-binary-focused-85bb44a.json +154 -0
- package/docs/native-terminal-semantics-evidence/final-binary-full-incomplete-85bb44a.json +160 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-85bb44a.json +149 -0
- package/docs/native-terminal-semantics-evidence/final-binary-layout-interactive-85bb44a.json +159 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-escape-unchanged-85bb44a.json +44 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-exact-identity-85bb44a.json +47 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-picker-unchanged-model-incomplete-85bb44a.json +58 -0
- package/docs/native-terminal-semantics-evidence/final-binary-model-selftest.json +16 -0
- package/docs/native-terminal-semantics-evidence/final-binary-picker-cancel-incomplete-85bb44a.json +62 -0
- package/docs/native-terminal-semantics-evidence/focused-clipboard-title-stale-binary.json +161 -0
- package/docs/native-terminal-semantics-evidence/layout-stale-binary-observation-not-accepted.json +153 -0
- package/docs/native-terminal-semantics-evidence/picker-latest-incomplete-attempt.json +62 -0
- package/docs/native-terminal-semantics-evidence/picker-previous-probe-pass.json +61 -0
- package/docs/native-terminal-semantics.md +89 -0
- package/docs/native-upgrade.md +177 -0
- package/docs/opencode-composed-source-review.md +89 -0
- package/docs/opencode-migration.md +225 -9
- package/docs/opencode-owned-runtime.md +241 -0
- package/docs/opencode-runtime.md +12 -0
- package/docs/opencode-theme-provider-proof/README.md +55 -0
- package/docs/opencode-theme-provider-proof/live-theme-api-base.patch +122 -0
- package/docs/opencode-theme-provider-proof/plugin-theme-provider.patch +107 -0
- package/docs/opencode-theme-provider-proof/session-ui-base.patch +1217 -0
- package/docs/owned-host-controls-proof.md +39 -0
- package/docs/owned-runtime-release-manifest.md +42 -0
- package/docs/owned-visual-clear-proof.md +40 -0
- package/docs/patches/opencode-v2.0.32-glyph-spinner-test-isolation-v1.patch +50 -0
- package/docs/patches/opencode-v2.0.32-glyphs.patch +3670 -0
- package/docs/patches/opencode-v2.0.32-help-dispatch.patch +156 -0
- package/docs/patches/opencode-v2.0.32-live-theme-api.patch +122 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v1.patch +5272 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v2.patch +6052 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v3.patch +8657 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v4.patch +9928 -0
- package/docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch +13965 -0
- package/docs/patches/opencode-v2.0.32-session-ui.patch +5137 -0
- package/docs/release-notes-native-draft.md +73 -0
- package/docs/release-notes-preview.10.md +121 -0
- package/docs/release-notes-preview.11.md +76 -0
- package/docs/release-notes-preview.12.md +55 -0
- package/docs/release-notes-preview.13.md +68 -0
- package/docs/release-notes-preview.9.md +130 -0
- package/docs/source-preview.md +120 -0
- package/evaluate.mjs +28 -1
- package/lib/budget.mjs +1 -1
- package/lib/codegraph.mjs +1 -1
- package/lib/delivery-review.mjs +11 -1
- package/lib/delivery.mjs +3 -2
- package/lib/environment.mjs +8 -0
- package/lib/guidance.mjs +1 -1
- package/lib/opencode/cli-runtime.mjs +1171 -0
- package/lib/opencode/cli.mjs +252 -0
- package/lib/opencode/credentials.mjs +63 -8
- package/lib/opencode/demo.mjs +53 -0
- package/lib/opencode/evaluation-budget-plugin/index.ts +3 -0
- package/lib/opencode/evaluation.mjs +32 -12
- package/lib/opencode/headless.mjs +8 -4
- package/lib/opencode/provider-policy.mjs +23 -0
- package/lib/opencode/review.mjs +37 -16
- package/lib/opencode/runtime-artifact.mjs +335 -0
- package/lib/opencode/runtime-github-transport.mjs +121 -0
- package/lib/opencode/runtime-release.json +36 -0
- package/lib/opencode/runtime.mjs +299 -20
- package/lib/opencode/service.mjs +77 -35
- package/lib/opencode/session-options.mjs +6 -2
- package/lib/opencode/update.mjs +222 -0
- package/lib/opencode/workspace.mjs +1 -1
- package/lib/planning-advice.mjs +2 -2
- package/lib/providers/experiential.mjs +2 -2
- package/lib/tui/framing.mjs +1 -80
- package/lib/update.mjs +10 -1
- package/package.json +33 -7
- package/plugins/carthagent/README.md +23 -0
- package/plugins/carthagent/src/bootstrap.ts +1 -1
- package/plugins/carthagent/src/daily-evidence.mjs +57 -0
- package/plugins/carthagent/src/daily-quests-rpc.ts +50 -0
- package/plugins/carthagent/src/daily-source-rpc.ts +52 -0
- package/plugins/carthagent/src/index.ts +71 -24
- package/plugins/carthagent/src/rpc.ts +51 -6
- package/plugins/carthagent/src/server/codegraph.ts +12 -5
- package/plugins/carthagent/src/server/daily-quests-rpc.ts +277 -0
- package/plugins/carthagent/src/server/daily-source-rpc.ts +211 -0
- package/plugins/carthagent/src/server/delivery.ts +227 -112
- package/plugins/carthagent/src/server/framing.mjs +80 -0
- package/plugins/carthagent/src/server/policy.ts +1 -1
- package/plugins/carthagent/src/server/providers.ts +59 -31
- package/plugins/carthagent/src/server/session-controls.ts +3 -1
- package/plugins/carthagent/src/server/ship-agents.ts +77 -0
- package/plugins/carthagent/src/server/ship-context.mjs +60 -0
- package/plugins/carthagent/src/server/ship-guidance.ts +41 -0
- package/plugins/carthagent/src/server/ship-rpc.ts +69 -0
- package/plugins/carthagent/src/server/voyages-rpc.ts +223 -0
- package/plugins/carthagent/src/testing.ts +5 -0
- package/plugins/carthagent/src/tui.tsx +496 -0
- package/plugins/carthagent/src/ui/client.ts +125 -0
- package/plugins/carthagent/src/ui/commands.ts +70 -0
- package/plugins/carthagent/src/ui/daily-editor-focus.mjs +52 -0
- package/plugins/carthagent/src/ui/daily-editor.mjs +4 -0
- package/plugins/carthagent/src/ui/daily-quests-events.mjs +18 -0
- package/plugins/carthagent/src/ui/daily-quests-state.mjs +218 -0
- package/plugins/carthagent/src/ui/daily-quests.tsx +235 -0
- package/plugins/carthagent/src/ui/host-state.ts +42 -0
- package/plugins/carthagent/src/ui/model-command.ts +32 -0
- package/plugins/carthagent/src/ui/native-navigation.ts +23 -0
- package/plugins/carthagent/src/ui/native-review.d.mts +33 -0
- package/plugins/carthagent/src/ui/native-review.mjs +159 -0
- package/plugins/carthagent/src/ui/resume-session.mjs +34 -0
- package/plugins/carthagent/src/ui/select-mouse.ts +17 -0
- package/plugins/carthagent/src/ui/session-preferences.d.mts +7 -0
- package/plugins/carthagent/src/ui/session-preferences.mjs +14 -0
- package/plugins/carthagent/src/ui/theme-provision.d.mts +3 -0
- package/plugins/carthagent/src/ui/theme-provision.mjs +134 -0
- package/plugins/carthagent/src/ui/voyages-navigation-state.mjs +56 -0
- package/plugins/carthagent/src/ui/voyages-navigation.tsx +131 -0
- package/plugins/carthagent/src/ui/voyages-state.mjs +142 -0
- package/plugins/carthagent/src/ui/voyages.tsx +129 -0
- package/plugins/carthagent/src/ui/workspace-live.ts +61 -0
- package/plugins/carthagent/src/ui/workspace.tsx +379 -0
- package/plugins/carthagent/src/voyages-rpc.ts +60 -0
- package/plugins/carthagent/src/workspace-rpc.ts +36 -0
- package/plugins/carthagent/tests/commands.test.ts +107 -0
- package/plugins/carthagent/tests/session-preferences.test.mjs +41 -0
- package/plugins/carthagent/tests/voyages-navigation.test.tsx +269 -0
- package/plugins/carthagent/themes/carthage.json +67 -0
- package/plugins/carthagent/tui.ts +26 -1
- package/script/ctg/build-owned-runtime.sh +166 -0
- package/tests/budget-engine.test.mjs +4 -3
- package/tests/cli.test.mjs +52 -96
- package/tests/daily-evidence.test.mjs +62 -0
- package/tests/delivery-review.test.mjs +11 -0
- package/tests/delivery.test.mjs +22 -1
- package/tests/fixtures/native-discovery-plugin/server.ts +15 -0
- package/tests/generate-owned-runtime-release.test.mjs +354 -0
- package/tests/installed-runtime-contract.test.mjs +43 -0
- package/tests/native-cli-host-smoke.mjs +457 -0
- package/tests/native-daily-editor-focus.test.mjs +213 -0
- package/tests/native-daily-editor.test.mjs +43 -0
- package/tests/native-daily-quests-layout.test.mjs +53 -0
- package/tests/native-daily-quests-rpc.test.mjs +456 -0
- package/tests/native-daily-quests-ui.test.mjs +778 -0
- package/tests/native-daily-source.test.mjs +451 -0
- package/tests/native-demo-ui.test.mjs +48 -0
- package/tests/native-evaluation-admission.test.mjs +24 -0
- package/tests/native-fixture-browser-cleanup.test.mjs +65 -0
- package/tests/native-fixture-runtime.test.mjs +111 -0
- package/tests/native-fixture-stop-diagnostics.test.mjs +67 -0
- package/tests/native-glyph-pty.test.mjs +107 -0
- package/tests/native-manual-projects.test.mjs +170 -0
- package/tests/native-model-command.test.mjs +67 -0
- package/tests/native-owned-runtime-env.test.mjs +140 -0
- package/tests/native-paired-guards.test.mjs +464 -0
- package/tests/native-provider-policy.test.mjs +31 -0
- package/tests/native-pty-invocation.test.mjs +114 -0
- package/tests/native-pty-runtime.test.mjs +45 -0
- package/tests/native-resume-session.test.mjs +46 -0
- package/tests/native-review-consumer.test.mjs +286 -0
- package/tests/native-runtime-identity.test.mjs +214 -0
- package/tests/native-select-mouse.test.mjs +23 -0
- package/tests/native-ship-parity.test.mjs +526 -0
- package/tests/native-ship-plan.test.mjs +85 -0
- package/tests/native-ship-rpc.test.mjs +71 -0
- package/tests/native-ui-commands.test.mjs +6 -0
- package/tests/native-voyages-layout.test.mjs +28 -0
- package/tests/native-voyages-navigation.test.mjs +169 -0
- package/tests/native-voyages-rpc.test.mjs +395 -0
- package/tests/native-voyages-ui.test.mjs +277 -0
- package/tests/opencode-atomic-write.test.mjs +55 -0
- package/tests/opencode-cli.test.mjs +1312 -0
- package/tests/opencode-codegraph-plugin.test.mjs +47 -0
- package/tests/opencode-delivery.test.mjs +16 -0
- package/tests/opencode-providers.test.mjs +245 -18
- package/tests/opencode-runtime.test.mjs +546 -8
- package/tests/opencode-session-controls.test.mjs +15 -0
- package/tests/opencode-ship-rpc.test.mjs +71 -0
- package/tests/opencode-update.test.mjs +475 -0
- package/tests/opencode-workspace.test.mjs +51 -2
- package/tests/owned-runtime-publish-workflow.test.mjs +477 -0
- package/tests/release-workflow.test.mjs +46 -0
- package/tests/runtime-artifact.test.mjs +269 -0
- package/tests/runtime-development-trust.test.mjs +230 -0
- package/tests/runtime-github-transport.test.mjs +132 -0
- package/tests/runtime-token-env.test.mjs +23 -0
- package/tests/server.test.mjs +3 -1
- package/tests/ship-telemetry.test.mjs +1 -1
- package/tests/source-preview.test.mjs +253 -0
- package/tests/test_native_terminal_probe.py +76 -0
- package/tests/test_native_terminal_semantics.py +232 -0
- package/tests/update.test.mjs +42 -5
- package/tests/verify-draft-runtime-gate.test.mjs +280 -0
- package/tests/verify-owned-runtime-release.test.mjs +371 -0
- package/tests/workspace-live.test.mjs +127 -0
- package/tools/benchmark/diagnose-request.mjs +59 -36
- package/tools/benchmark/eval-common.mjs +3 -3
- package/tools/benchmark/evaluators.test.mjs +4 -4
- package/tools/benchmark/guard.test.mjs +35 -2
- package/tools/benchmark/guard.ts +20 -7
- package/tools/benchmark/planning-startup.test.mjs +30 -34
- package/tools/benchmark/runner.mjs +12 -7
- package/tools/benchmark/runner.test.mjs +23 -7
- package/tools/benchmark/upstream-error.test.mjs +16 -2
- package/tools/generate-owned-runtime-release.mjs +217 -0
- package/tools/installed-runtime-contract.mjs +16 -0
- package/tools/manual-runtime-manifest.mjs +39 -0
- package/tools/native-fixture-browser-cleanup.mjs +59 -0
- package/tools/native-fixture-stop-diagnostics.mjs +43 -0
- package/tools/native-fixture.mjs +199 -40
- package/tools/native-glyph-helpers.py +127 -0
- package/tools/native-glyph-pty.py +144 -0
- package/tools/native-manual-projects.mjs +259 -0
- package/tools/native-manual.mjs +338 -0
- package/tools/native-model-picker-acceptance.py +306 -0
- package/tools/native-owned-runtime-env.sh +47 -0
- package/tools/native-paired-guards.mjs +282 -0
- package/tools/native-paired-manifest.mjs +72 -0
- package/tools/native-paired-orchestration.sh +51 -0
- package/tools/native-plugin-readiness.mjs +33 -0
- package/tools/native-plugin-readiness.test.mjs +53 -0
- package/tools/native-pty-invocation.mjs +26 -0
- package/tools/native-pty-runtime.mjs +83 -0
- package/tools/native-pty-smoke.py +147 -24
- package/tools/native-public-repo-shell-result.mjs +13 -0
- package/tools/native-public-repo-shell-result.test.mjs +28 -0
- package/tools/native-runtime-identity.mjs +201 -0
- package/tools/native-slash-acceptance-env.py +38 -0
- package/tools/native-terminal-semantics.py +876 -0
- package/tools/owned-shell-observer/index.ts +16 -0
- package/tools/owned-shell-observer/package.json +1 -0
- package/tools/owned-visual-clear-env.py +48 -0
- package/tools/owned-visual-clear-model.py +21 -0
- package/tools/pinned-owned-runtime-inputs.mjs +46 -0
- package/tools/prepare-native-manual.mjs +122 -0
- package/tools/run-native-terminal-paired.sh +403 -0
- package/tools/runtime-token-env.mjs +8 -0
- package/tools/seed-native-switch-fixture.mjs +79 -0
- package/tools/test-native-controls.mjs +3 -1
- package/tools/test-native-daily-quests.mjs +65 -0
- package/tools/test-native-default-cli.mjs +106 -0
- package/tools/test-native-delivery-host.mjs +102 -20
- package/tools/test-native-evaluation.mjs +7 -3
- package/tools/test-native-glyph-pty.mjs +94 -0
- package/tools/test-native-host.mjs +87 -63
- package/tools/test-native-outcomes.mjs +3 -1
- package/tools/test-native-package.mjs +206 -38
- package/tools/test-native-performance.mjs +26 -0
- package/tools/test-native-product.mjs +90 -0
- package/tools/test-native-pty.mjs +34 -9
- package/tools/test-native-public-repo.mjs +224 -0
- package/tools/test-native-refresh-reentry.mjs +106 -0
- package/tools/test-native-report.mjs +27 -20
- package/tools/test-native-review.mjs +75 -0
- package/tools/test-native-runtime.mjs +120 -0
- package/tools/test-native-ship-models.mjs +176 -0
- package/tools/test-native-ship-walkthrough.mjs +96 -0
- package/tools/test-native-slash-commands.mjs +125 -0
- package/tools/test-native-slash-commands.py +242 -0
- package/tools/test-native-swe-host.mjs +224 -55
- package/tools/test-native-tasks.mjs +77 -0
- package/tools/test-native-terminal-performance.py +887 -0
- package/tools/test-native-terminal-readiness.sh +85 -0
- package/tools/test-native-terminal-semantics-installed.mjs +38 -0
- package/tools/test-native-visual-clear-installed.mjs +48 -0
- package/tools/test-owned-host-controls.mjs +238 -0
- package/tools/test-owned-opencode-runtime.mjs +35 -0
- package/tools/test-owned-visual-clear-env.py +120 -0
- package/tools/test-owned-visual-clear.py +373 -0
- package/tools/verify-draft-runtime-gate.mjs +138 -0
- package/tools/verify-owned-runtime-release.mjs +114 -0
- package/tools/verify-release-version.mjs +21 -1
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { gzipSync } from 'node:zlib';
|
|
10
|
+
|
|
11
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const workflowPath = path.join(root, '.github/workflows/publish-owned-opencode-runtime.yml');
|
|
13
|
+
const workflow = await readFile(workflowPath, 'utf8');
|
|
14
|
+
const workflowPathName = '.github/workflows/publish-owned-opencode-runtime.yml';
|
|
15
|
+
const sourceCommit = '843a6c514e606b436a7fe713422ace0a93ad3d61';
|
|
16
|
+
const lockSha256 = '9d78379cd264584a495440a217b9d2ecab524c9a56f7f8b4b5bed3926daa330d';
|
|
17
|
+
const releaseVersion = '1.2.3';
|
|
18
|
+
const releaseTag = `v${releaseVersion}`;
|
|
19
|
+
const workflowId = 987;
|
|
20
|
+
const runId = 4242;
|
|
21
|
+
const runSha = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
22
|
+
const tagObject = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';
|
|
23
|
+
const tagCommit = 'cccccccccccccccccccccccccccccccccccccccc';
|
|
24
|
+
const requiresUbuntuWorkflowShell = process.platform !== 'linux'
|
|
25
|
+
? 'the workflow steps under test run on ubuntu-latest and require Bash/GNU checksum utilities'
|
|
26
|
+
: false;
|
|
27
|
+
const targets = [
|
|
28
|
+
['linux-x64', 'linux', 'x64', 'opencode-linux-x64.tar.gz', 'cli-linux-x64/bin/opencode'],
|
|
29
|
+
['linux-arm64', 'linux', 'arm64', 'opencode-linux-arm64.tar.gz', 'cli-linux-arm64/bin/opencode'],
|
|
30
|
+
['darwin-x64', 'darwin', 'x64', 'opencode-darwin-x64.tar.gz', 'cli-darwin-x64/bin/opencode'],
|
|
31
|
+
['darwin-arm64', 'darwin', 'arm64', 'opencode-darwin-arm64.tar.gz', 'cli-darwin-arm64/bin/opencode'],
|
|
32
|
+
['windows-x64', 'windows', 'x64', 'opencode-windows-x64.tar.gz', 'cli-windows-x64/bin/opencode.exe'],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function extractRunBlock(selector) {
|
|
36
|
+
const lines = workflow.split(/\r?\n/);
|
|
37
|
+
const selectorIndex = lines.findIndex((line) => selector(lines, line));
|
|
38
|
+
assert.notEqual(selectorIndex, -1, 'workflow step selector must match');
|
|
39
|
+
const selectorIndent = lines[selectorIndex].match(/^ */)?.[0].length ?? 0;
|
|
40
|
+
let stepStart = -1;
|
|
41
|
+
let stepIndent = -1;
|
|
42
|
+
for (let index = selectorIndex; index >= 0; index -= 1) {
|
|
43
|
+
const match = lines[index].match(/^( *)- (?:name|uses):/);
|
|
44
|
+
if (match && match[1].length < selectorIndent + 2) {
|
|
45
|
+
stepStart = index;
|
|
46
|
+
stepIndent = match[1].length;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
assert.notEqual(stepStart, -1, 'selected workflow line must belong to a step');
|
|
51
|
+
let stepEnd = lines.length;
|
|
52
|
+
const nextStep = new RegExp(`^ {${stepIndent}}- (?:name|uses):`);
|
|
53
|
+
for (let index = stepStart + 1; index < lines.length; index += 1) {
|
|
54
|
+
if (nextStep.test(lines[index])) { stepEnd = index; break; }
|
|
55
|
+
}
|
|
56
|
+
const runIndex = lines.findIndex((line, index) => index > selectorIndex && index < stepEnd && /^\s+run: \|\s*$/.test(line));
|
|
57
|
+
assert.notEqual(runIndex, -1, 'selected workflow step must use a literal run block');
|
|
58
|
+
const indent = lines[runIndex + 1].match(/^ */)?.[0].length;
|
|
59
|
+
assert.ok(Number.isInteger(indent) && indent > 0, 'run block must have indented content');
|
|
60
|
+
const body = [];
|
|
61
|
+
for (let index = runIndex + 1; index < lines.length; index += 1) {
|
|
62
|
+
const line = lines[index];
|
|
63
|
+
if (line.trim() && (line.match(/^ */)?.[0].length ?? 0) < indent) break;
|
|
64
|
+
body.push(line.trim() ? line.slice(indent) : '');
|
|
65
|
+
}
|
|
66
|
+
return body.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const validateBlock = extractRunBlock((_lines, line) => line.trim() === 'id: validate');
|
|
70
|
+
const verifyBlock = extractRunBlock((_lines, line) => line.trim() === 'id: verify');
|
|
71
|
+
const uploadBlock = extractRunBlock((_lines, line) => line.includes('name: Recheck immutable draft target and upload verified bytes without clobbering'));
|
|
72
|
+
const archiveBlock = extractRunBlock((_lines, line) => line.includes('name: Archive runtime bytes and write native build record'));
|
|
73
|
+
|
|
74
|
+
test('native archive command uses a relative output path safe for Windows drive-letter workspaces', { skip: requiresUbuntuWorkflowShell }, async (t) => {
|
|
75
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'ctg-owned-runtime-archive-test-'));
|
|
76
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
77
|
+
const workspace = path.join(directory, 'D:', 'a', 'Carthagent');
|
|
78
|
+
await mkdir(path.join(workspace, 'candidate-assets'), { recursive: true });
|
|
79
|
+
await mkdir(path.join(workspace, 'opencode/packages/cli/dist-ctg/cli-windows-x64/bin'), { recursive: true });
|
|
80
|
+
await writeFile(path.join(workspace, 'opencode/packages/cli/dist-ctg/cli-windows-x64/bin/opencode.exe'), 'fixture-runtime');
|
|
81
|
+
|
|
82
|
+
const archiveLines = archiveBlock.split('\n').filter((line) => line.startsWith('archive=') || line.startsWith('COPYFILE_DISABLE=1 tar '));
|
|
83
|
+
assert.equal(archiveLines.length, 2, 'archive fixture must execute the archive path and tar command from the workflow block');
|
|
84
|
+
assert.equal(archiveLines[0], 'archive="candidate-assets/opencode-${TARGET_KEY}.tar.gz"');
|
|
85
|
+
assert.equal(archiveLines[1], 'COPYFILE_DISABLE=1 tar -C opencode/packages/cli/dist-ctg --format=ustar -czf "$archive" .');
|
|
86
|
+
|
|
87
|
+
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', archiveLines.join('\n')], {
|
|
88
|
+
cwd: workspace,
|
|
89
|
+
env: { ...process.env, TARGET_KEY: 'windows-x64' },
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
});
|
|
92
|
+
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
93
|
+
const archivePath = path.join(workspace, 'candidate-assets/opencode-windows-x64.tar.gz');
|
|
94
|
+
const entries = execFileSync('tar', ['-tzf', archivePath], { encoding: 'utf8' }).trim().split('\n');
|
|
95
|
+
assert.ok(entries.includes('./cli-windows-x64/bin/opencode.exe'));
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
function workflowJob(name) {
|
|
99
|
+
const lines = workflow.split(/\r?\n/);
|
|
100
|
+
const start = lines.findIndex((line) => line === ` ${name}:`);
|
|
101
|
+
assert.notEqual(start, -1, `workflow job ${name} must exist`);
|
|
102
|
+
let end = lines.length;
|
|
103
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
104
|
+
if (/^ [A-Za-z0-9_-]+:$/.test(lines[index])) { end = index; break; }
|
|
105
|
+
}
|
|
106
|
+
return lines.slice(start, end).join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const combinedPatchPath = 'docs/patches/opencode-v2.0.32-session-ui-combined-v5.patch';
|
|
110
|
+
|
|
111
|
+
function sha256(bytes) {
|
|
112
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function octal(value, width) {
|
|
116
|
+
return Buffer.from(`${value.toString(8).padStart(width - 1, '0')}\0`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function tarEntry(name, data, type = '0', mode = 0o644) {
|
|
120
|
+
const header = Buffer.alloc(512);
|
|
121
|
+
header.write(name, 0, 100);
|
|
122
|
+
octal(mode, 8).copy(header, 100);
|
|
123
|
+
octal(0, 8).copy(header, 108);
|
|
124
|
+
octal(0, 8).copy(header, 116);
|
|
125
|
+
octal(data.length, 12).copy(header, 124);
|
|
126
|
+
octal(0, 12).copy(header, 136);
|
|
127
|
+
header.fill(32, 148, 156);
|
|
128
|
+
header[156] = type.charCodeAt(0);
|
|
129
|
+
header.write('ustar\0', 257, 6);
|
|
130
|
+
header.write('00', 263, 2);
|
|
131
|
+
let checksum = 0;
|
|
132
|
+
for (const byte of header) checksum += byte;
|
|
133
|
+
octal(checksum, 8).copy(header, 148);
|
|
134
|
+
return Buffer.concat([header, data, Buffer.alloc((512 - (data.length % 512)) % 512)]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function makeRuntimeArchive(key, platform, arch, executablePath, executableBytes, runtimeManifest) {
|
|
138
|
+
const parentEntries = executablePath.split('/').slice(0, -1).map((_, index, parts) => {
|
|
139
|
+
const name = `${parts.slice(0, index + 1).join('/')}/`;
|
|
140
|
+
return tarEntry(name, Buffer.alloc(0), '5');
|
|
141
|
+
});
|
|
142
|
+
const packageJson = Buffer.from(JSON.stringify({
|
|
143
|
+
name: `@opencode/cli-${key}`,
|
|
144
|
+
version: '0.0.0-ctg.2.0.32',
|
|
145
|
+
os: [platform === 'windows' ? 'win32' : platform],
|
|
146
|
+
cpu: [arch],
|
|
147
|
+
}));
|
|
148
|
+
const sums = Buffer.from(`${sha256(executableBytes)} ${executablePath}\n`);
|
|
149
|
+
return gzipSync(Buffer.concat([
|
|
150
|
+
tarEntry('./', Buffer.alloc(0), '5'),
|
|
151
|
+
...parentEntries,
|
|
152
|
+
tarEntry(executablePath, executableBytes, '0', 0o755),
|
|
153
|
+
tarEntry(`cli-${key}/package.json`, packageJson),
|
|
154
|
+
tarEntry('LICENSE', Buffer.from('MIT License\n')),
|
|
155
|
+
tarEntry('SHA256SUMS', sums),
|
|
156
|
+
tarEntry('ctg-runtime.json', Buffer.from(JSON.stringify(runtimeManifest))),
|
|
157
|
+
Buffer.alloc(1024),
|
|
158
|
+
]));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function fixture(t) {
|
|
162
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), 'ctg-owned-runtime-publish-test-'));
|
|
163
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
164
|
+
const candidate = path.join(directory, 'candidate');
|
|
165
|
+
const ctg = path.join(directory, 'ctg');
|
|
166
|
+
const generator = path.join(directory, 'release-generator/tools/generate-owned-runtime-release.mjs');
|
|
167
|
+
const fakeBin = path.join(directory, 'bin');
|
|
168
|
+
const capture = path.join(directory, 'upload-capture');
|
|
169
|
+
await Promise.all([
|
|
170
|
+
mkdir(candidate), mkdir(path.join(ctg, 'lib/opencode'), { recursive: true }),
|
|
171
|
+
mkdir(path.join(ctg, 'script/ctg'), { recursive: true }), mkdir(path.join(ctg, 'docs/patches'), { recursive: true }),
|
|
172
|
+
mkdir(path.dirname(generator), { recursive: true }), mkdir(fakeBin), mkdir(capture),
|
|
173
|
+
]);
|
|
174
|
+
await writeFile(path.join(ctg, 'package.json'), JSON.stringify({ name: 'fixture', version: releaseVersion }));
|
|
175
|
+
const patch = await readFile(path.join(root, combinedPatchPath));
|
|
176
|
+
const patchSha256 = sha256(patch);
|
|
177
|
+
await Promise.all([
|
|
178
|
+
copyFile(path.join(root, 'script/ctg/build-owned-runtime.sh'), path.join(ctg, 'script/ctg/build-owned-runtime.sh')),
|
|
179
|
+
copyFile(path.join(root, combinedPatchPath), path.join(ctg, combinedPatchPath)),
|
|
180
|
+
]);
|
|
181
|
+
execFileSync('git', ['init', '-q'], { cwd: ctg });
|
|
182
|
+
execFileSync('git', ['-c', 'user.name=Workflow Fixture', '-c', 'user.email=fixture@example.invalid', 'add', combinedPatchPath, 'script/ctg/build-owned-runtime.sh'], { cwd: ctg });
|
|
183
|
+
execFileSync('git', ['-c', 'user.name=Workflow Fixture', '-c', 'user.email=fixture@example.invalid', 'commit', '-qm', 'fixture candidate patch'], { cwd: ctg });
|
|
184
|
+
await copyFile(path.join(root, 'lib/opencode/runtime.mjs'), path.join(ctg, 'lib/opencode/runtime.mjs'));
|
|
185
|
+
|
|
186
|
+
const manifest = {
|
|
187
|
+
version: 1,
|
|
188
|
+
releaseVersion,
|
|
189
|
+
compatibilityVersion: '2.0.32',
|
|
190
|
+
runtimeVersion: '0.0.0-ctg.2.0.32',
|
|
191
|
+
sourceRevision: sourceCommit,
|
|
192
|
+
patchSha256,
|
|
193
|
+
lockSha256,
|
|
194
|
+
assets: {},
|
|
195
|
+
};
|
|
196
|
+
for (const [key, platform, arch, name, executablePath] of targets) {
|
|
197
|
+
const executableBytes = Buffer.from(`tiny test executable for ${key}\n`);
|
|
198
|
+
const runtimeMetadata = {
|
|
199
|
+
owner: 'Carthagent',
|
|
200
|
+
sourceRepository: 'https://github.com/anomalyco/opencode',
|
|
201
|
+
sourceRevision: sourceCommit,
|
|
202
|
+
lockSha256,
|
|
203
|
+
patchSha256,
|
|
204
|
+
compatibilityVersion: '2.0.32',
|
|
205
|
+
runtimeVersion: '0.0.0-ctg.2.0.32',
|
|
206
|
+
channel: 'ctg.2.0.32',
|
|
207
|
+
platform: `${platform}-${arch}`,
|
|
208
|
+
webUI: false,
|
|
209
|
+
};
|
|
210
|
+
const archiveBytes = makeRuntimeArchive(key, platform, arch, executablePath, executableBytes, runtimeMetadata);
|
|
211
|
+
const record = {
|
|
212
|
+
version: 1, target: key, platform, arch, sourceRevision: sourceCommit, patchSha256, lockSha256,
|
|
213
|
+
compatibilityVersion: '2.0.32', runtimeVersion: '0.0.0-ctg.2.0.32',
|
|
214
|
+
executableSha256: sha256(executableBytes),
|
|
215
|
+
};
|
|
216
|
+
await writeFile(path.join(candidate, name), archiveBytes);
|
|
217
|
+
await writeFile(path.join(candidate, `${name}.build.json`), JSON.stringify(record));
|
|
218
|
+
manifest.assets[key] = { name, archiveSha256: sha256(archiveBytes), executableSha256: sha256(executableBytes) };
|
|
219
|
+
}
|
|
220
|
+
const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
221
|
+
await writeFile(path.join(ctg, 'lib/opencode/runtime-release.json'), manifestBytes);
|
|
222
|
+
await writeFile(path.join(candidate, 'runtime-release.json.candidate'), manifestBytes);
|
|
223
|
+
const archiveNames = targets.map((entry) => entry[3]).sort();
|
|
224
|
+
const archiveSums = [];
|
|
225
|
+
for (const name of archiveNames) archiveSums.push(`${sha256(await readFile(path.join(candidate, name)))} ${name}`);
|
|
226
|
+
await writeFile(path.join(candidate, 'runtime-archives-SHA256SUMS.txt'), `${archiveSums.join('\n')}\n`);
|
|
227
|
+
await writeFile(path.join(candidate, 'candidate-provenance.json'), JSON.stringify({
|
|
228
|
+
version: 1, releaseVersion, sourceCommit: runSha,
|
|
229
|
+
workflowPath: workflowPathName, runId,
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
// The real workflow shell blocks are evaluated verbatim. This generator
|
|
233
|
+
// fixture models only the CLI contract: five archive/record pairs, the
|
|
234
|
+
// recipe-to-patch digest binding, and manifest hashes derived from candidate
|
|
235
|
+
// archive bytes. It has no network or external package dependencies.
|
|
236
|
+
await writeFile(generator, `
|
|
237
|
+
import { createHash } from 'node:crypto';
|
|
238
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
239
|
+
import path from 'node:path';
|
|
240
|
+
const [version, directory, repo] = process.argv.slice(2);
|
|
241
|
+
const names = ${JSON.stringify(targets.map((entry) => entry[3]))};
|
|
242
|
+
const keys = ${JSON.stringify(targets.map((entry) => entry[0]))};
|
|
243
|
+
const platforms = ${JSON.stringify(targets.map((entry) => entry[1]))};
|
|
244
|
+
const arches = ${JSON.stringify(targets.map((entry) => entry[2]))};
|
|
245
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
246
|
+
const entries = await readdir(directory);
|
|
247
|
+
if(entries.length!==10 || names.some(n=>!entries.includes(n)||!entries.includes(n+'.build.json'))) throw new Error('expected exactly five runtime archive/build-record pairs');
|
|
248
|
+
const recipe=await readFile(path.join(repo,'script/ctg/build-owned-runtime.sh'),'utf8');
|
|
249
|
+
const patch=await readFile(path.join(repo,${JSON.stringify(combinedPatchPath)}));
|
|
250
|
+
const expected=recipe.match(/^EXPECTED_PATCH="([^"]+)"$/m)?.[1];
|
|
251
|
+
if(!expected || sha(patch)!==expected) throw new Error('recipe patch digest mismatch: expected='+expected+' actual='+sha(patch)+' path='+${JSON.stringify(combinedPatchPath)});
|
|
252
|
+
const runtimeSource=await readFile(path.join(repo,'lib/opencode/runtime.mjs'),'utf8');
|
|
253
|
+
const runtimePatch=runtimeSource.match(/CTG_RUNTIME_PATCH_SHA256 = '([a-f0-9]{64})'/)?.[1];
|
|
254
|
+
if(runtimePatch!==expected) throw new Error('fixture packaged runtime trust root patch digest mismatch');
|
|
255
|
+
const manifest={version:1,releaseVersion:version,compatibilityVersion:'2.0.32',runtimeVersion:'0.0.0-ctg.2.0.32',sourceRevision:'${sourceCommit}',patchSha256:expected,lockSha256:'${lockSha256}',assets:Object.fromEntries(keys.map((key,i)=>[key,{name:names[i],archiveSha256:'0'.repeat(64),executableSha256:'0'.repeat(64)}]))};
|
|
256
|
+
for(let i=0;i<names.length;i++) {
|
|
257
|
+
const record=JSON.parse(await readFile(path.join(directory,names[i]+'.build.json'),'utf8'));
|
|
258
|
+
if(record.target!==keys[i] || record.platform!==platforms[i] || record.arch!==arches[i] || record.sourceRevision!==manifest.sourceRevision || record.patchSha256!==expected || record.lockSha256!==manifest.lockSha256 || record.compatibilityVersion!==manifest.compatibilityVersion || record.runtimeVersion!==manifest.runtimeVersion) throw new Error('build record mismatch '+keys[i]);
|
|
259
|
+
manifest.assets[keys[i]].archiveSha256=sha(await readFile(path.join(directory,names[i])));
|
|
260
|
+
manifest.assets[keys[i]].executableSha256=record.executableSha256;
|
|
261
|
+
}
|
|
262
|
+
await writeFile(path.resolve(directory,'..','runtime-release.json.candidate'),JSON.stringify(manifest,null,2)+'\\n',{flag:'wx'});
|
|
263
|
+
`);
|
|
264
|
+
|
|
265
|
+
const fakeGh = `#!/usr/bin/env node
|
|
266
|
+
import { copyFile, appendFile } from 'node:fs/promises';
|
|
267
|
+
import path from 'node:path';
|
|
268
|
+
const a=process.argv.slice(2), q=a.indexOf('--jq')>=0?a[a.indexOf('--jq')+1]:'';
|
|
269
|
+
const run={status:'completed',conclusion:'success',event:'workflow_dispatch',head_repository:{full_name:'B-icy/Carthagent'},workflow_id:${workflowId},head_branch:'feature/reviewed-prebuild',head_sha:'${runSha}'};
|
|
270
|
+
if(a[0]==='api' && a[1].endsWith('/actions/runs/${runId}')) console.log(JSON.stringify(run));
|
|
271
|
+
else if(a[0]==='api' && a[1].endsWith('/actions/workflows/publish-owned-opencode-runtime.yml')) console.log('${workflowId}');
|
|
272
|
+
else if(a[0]==='api' && a[1]==='repos/B-icy/Carthagent') console.log('main');
|
|
273
|
+
else if(a[0]==='api' && a[1].endsWith('/git/ref/tags/${releaseTag}')) {
|
|
274
|
+
if(q==='.object.sha') console.log('${tagObject}');
|
|
275
|
+
else if(q==='.object.type') console.log('tag');
|
|
276
|
+
else console.log(JSON.stringify({object:{sha:'${tagObject}',type:'tag'}}));
|
|
277
|
+
}
|
|
278
|
+
else if(a[0]==='api' && a[1].endsWith('/git/tags/${tagObject}')) {
|
|
279
|
+
if(q==='.object.sha') console.log(process.env.FIXTURE_TAG_COMMIT||'${tagCommit}');
|
|
280
|
+
else console.log(JSON.stringify({object:{sha:process.env.FIXTURE_TAG_COMMIT||'${tagCommit}',type:'commit'}}));
|
|
281
|
+
}
|
|
282
|
+
else if(a[0]==='api' && a[1].endsWith('/commits/${runSha}')) {
|
|
283
|
+
if(q==='.sha') console.log('${runSha}'); else console.log(JSON.stringify({sha:'${runSha}'}));
|
|
284
|
+
}
|
|
285
|
+
else if(a[0]==='release' && a[1]==='view' && a.includes('isDraft')) console.log('true');
|
|
286
|
+
else if(a[0]==='release' && a[1]==='view' && a.includes('assets')) console.log('');
|
|
287
|
+
else if(a[0]==='release' && a[1]==='upload') {
|
|
288
|
+
const item=a.find(x=>x.includes('#')); if(!item) throw new Error('missing upload source#name');
|
|
289
|
+
const [src,name]=item.split('#'); await copyFile(src,path.join(process.env.FIXTURE_UPLOAD_CAPTURE,name));
|
|
290
|
+
await appendFile(path.join(process.env.FIXTURE_UPLOAD_CAPTURE,'uploads.log'),name+'\\n');
|
|
291
|
+
} else { console.error('blocked unexpected fake gh invocation:',a.join(' ')); process.exit(97); }
|
|
292
|
+
`;
|
|
293
|
+
const jqSource = `
|
|
294
|
+
let s=''; for await (const c of process.stdin) s+=c;
|
|
295
|
+
const expr=process.argv.at(-1), o=JSON.parse(s); let v=o;
|
|
296
|
+
if(expr==='.isDraft == true') { console.log(o.isDraft===true?'true':'false'); process.exit(0); }
|
|
297
|
+
if(expr==='[.assets[].name] | join("\\\\n")') { console.log((o.assets||[]).map(x=>x.name).join('\\\\n')); process.exit(0); }
|
|
298
|
+
if(expr==='.sha') { console.log(o.sha); process.exit(0); }
|
|
299
|
+
for(const k of expr.replace(/^\\./,'').split('.')) v=v[k];
|
|
300
|
+
console.log(v);
|
|
301
|
+
`;
|
|
302
|
+
const fakeGhPath = path.join(fakeBin, 'fake-gh.mjs');
|
|
303
|
+
const fakeJqPath = path.join(fakeBin, 'fake-jq.mjs');
|
|
304
|
+
await writeFile(fakeGhPath, fakeGh);
|
|
305
|
+
await writeFile(fakeJqPath, jqSource);
|
|
306
|
+
await writeFile(path.join(fakeBin, 'gh'), `#!/bin/sh\nexec "${process.execPath}" "${fakeGhPath}" "$@"\n`);
|
|
307
|
+
await writeFile(path.join(fakeBin, 'jq'), `#!/bin/sh\nexec "${process.execPath}" "${fakeJqPath}" "$@"\n`);
|
|
308
|
+
await chmod(path.join(fakeBin, 'gh'), 0o755);
|
|
309
|
+
await chmod(path.join(fakeBin, 'jq'), 0o755);
|
|
310
|
+
return { directory, candidate, ctg, fakeBin, capture, manifestBytes };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function envFor(f, overrides = {}) {
|
|
314
|
+
const nodeExecPath = process.env.CTG_WORKFLOW_TEST_NODE ?? process.execPath;
|
|
315
|
+
const nodeDir = path.dirname(nodeExecPath);
|
|
316
|
+
return {
|
|
317
|
+
PATH: `${f.fakeBin}:${nodeDir}:/usr/bin:/bin`,
|
|
318
|
+
NODE_PATH: '',
|
|
319
|
+
BASH_ENV: '',
|
|
320
|
+
ENV: '',
|
|
321
|
+
HOME: f.directory,
|
|
322
|
+
GITHUB_REPOSITORY: 'B-icy/Carthagent',
|
|
323
|
+
GITHUB_WORKSPACE: f.directory,
|
|
324
|
+
GITHUB_REF: 'refs/heads/main',
|
|
325
|
+
GITHUB_SHA: 'dddddddddddddddddddddddddddddddddddddddd',
|
|
326
|
+
RELEASE_VERSION: releaseVersion,
|
|
327
|
+
RELEASE_TAG: releaseTag,
|
|
328
|
+
PREBUILD_RUN_ID: String(runId),
|
|
329
|
+
FIXTURE_UPLOAD_CAPTURE: f.capture,
|
|
330
|
+
GH_TOKEN: '',
|
|
331
|
+
...overrides,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function runBlock(script, cwd, env) {
|
|
336
|
+
const result = spawnSync('/usr/bin/bash', ['--noprofile', '--norc', '-e', '-u', '-o', 'pipefail', '-x', '-c', script], {
|
|
337
|
+
cwd,
|
|
338
|
+
env,
|
|
339
|
+
encoding: 'utf8',
|
|
340
|
+
timeout: 30_000,
|
|
341
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
342
|
+
});
|
|
343
|
+
return { ...result, output: `${result.stdout ?? ''}${result.stderr ?? ''}` };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function assertSuccess(result, label) {
|
|
347
|
+
assert.equal(result.error, undefined, `${label}: ${result.error?.message ?? ''}`);
|
|
348
|
+
assert.equal(result.status, 0, `${label} failed (signal=${result.signal}):\n${result.output}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function assertFailure(result, label) {
|
|
352
|
+
assert.equal(result.error, undefined, `${label}: ${result.error?.message ?? ''}`);
|
|
353
|
+
assert.notEqual(result.status, 0, `${label} unexpectedly succeeded`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function runPublishFixture(f, { tagCommitSha = tagCommit, tamper = false } = {}) {
|
|
357
|
+
const validateOutput = path.join(f.directory, 'validate-output');
|
|
358
|
+
const verifyOutput = path.join(f.directory, 'verify-output');
|
|
359
|
+
await writeFile(validateOutput, '');
|
|
360
|
+
await writeFile(verifyOutput, '');
|
|
361
|
+
if (tamper) await writeFile(path.join(f.candidate, targets[0][3]), Buffer.concat([await readFile(path.join(f.candidate, targets[0][3])), Buffer.from('tamper')]));
|
|
362
|
+
const baseEnv = envFor(f, { FIXTURE_TAG_COMMIT: tagCommitSha });
|
|
363
|
+
const validate = runBlock(validateBlock, f.directory, { ...baseEnv, MODE: 'publish', SOURCE_COMMIT: '', GITHUB_OUTPUT: validateOutput });
|
|
364
|
+
assertSuccess(validate, 'publish validate block');
|
|
365
|
+
const validated = await readFile(validateOutput, 'utf8');
|
|
366
|
+
assert.match(validated, new RegExp(`release_commit=${tagCommitSha}`));
|
|
367
|
+
|
|
368
|
+
const verify = runBlock(verifyBlock, f.directory, { ...baseEnv, GITHUB_OUTPUT: verifyOutput });
|
|
369
|
+
if (tamper) {
|
|
370
|
+
assertFailure(verify, 'tampered candidate verify block');
|
|
371
|
+
assert.match(verify.output, /FAILED|does NOT match/);
|
|
372
|
+
return { validate, verify, upload: undefined, verifyOutput };
|
|
373
|
+
}
|
|
374
|
+
assertSuccess(verify, 'publish verify block');
|
|
375
|
+
assert.equal(await readFile(verifyOutput, 'utf8'), 'candidate=verified\n');
|
|
376
|
+
const upload = runBlock(uploadBlock.replace('${{ needs.validate-request.outputs.release_commit }}', tagCommit), f.directory, baseEnv);
|
|
377
|
+
if (tagCommitSha !== tagCommit) {
|
|
378
|
+
assertFailure(upload, 'stale annotated tag upload block');
|
|
379
|
+
return { validate, verify, upload, verifyOutput };
|
|
380
|
+
}
|
|
381
|
+
assertSuccess(upload, 'publish upload block');
|
|
382
|
+
return { validate, verify, upload, verifyOutput };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
test('owned-runtime publish workflow extracts executable YAML run blocks', () => {
|
|
386
|
+
assert.match(validateBlock, /\[\[ "\$SOURCE_COMMIT" == "\$GITHUB_SHA" \]\]/);
|
|
387
|
+
assert.match(validateBlock, /refs\/heads\/\$default_branch/);
|
|
388
|
+
assert.match(verifyBlock, /candidate_dir=candidate/);
|
|
389
|
+
assert.match(verifyBlock, /candidate=verified/);
|
|
390
|
+
assert.match(uploadBlock, /candidate\/opencode-\$key/);
|
|
391
|
+
assert.doesNotMatch(uploadBlock, /candidate-bytes/);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('draft metadata validation has scoped write capability; artifact mutation stays in publish job', () => {
|
|
395
|
+
const validation = workflowJob('validate-request');
|
|
396
|
+
const prebuild = workflowJob('prebuild');
|
|
397
|
+
const assembly = workflowJob('assemble-candidate');
|
|
398
|
+
const publish = workflowJob('publish-existing-draft');
|
|
399
|
+
assert.match(validation, /permissions:\n actions: read\n contents: write/);
|
|
400
|
+
assert.match(validation, /gh release view "\$RELEASE_TAG".*--json isDraft/);
|
|
401
|
+
assert.doesNotMatch(validation, /gh release upload/);
|
|
402
|
+
assert.doesNotMatch(prebuild, /contents: write/);
|
|
403
|
+
assert.doesNotMatch(assembly, /contents: write/);
|
|
404
|
+
assert.match(publish, /contents: write/);
|
|
405
|
+
assert.match(uploadBlock, /gh release upload/);
|
|
406
|
+
assert.match(workflow, /git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch > opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch/);
|
|
407
|
+
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch\)"/);
|
|
408
|
+
assert.equal((workflow.match(/git -C ctg show HEAD:docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch > ctg\/docs\/patches\/opencode-v2\.0\.32-session-ui\.patch/g) ?? []).length, 1,
|
|
409
|
+
'candidate assembly must extract exact committed bytes at the pinned historical path');
|
|
410
|
+
assert.match(verifyBlock, /git -C ctg show HEAD:script\/ctg\/build-owned-runtime\.sh/);
|
|
411
|
+
assert.match(verifyBlock, /git -C ctg show "HEAD:\$patch_file" > ctg\/docs\/patches\/opencode-v2\.0\.32-session-ui\.patch/);
|
|
412
|
+
assert.match(workflow, /git -C ctg show HEAD:script\/ctg\/build-owned-runtime\.sh > opencode\/script\/ctg\/build-owned-runtime\.sh/);
|
|
413
|
+
assert.match(workflow, /chmod \+x opencode\/script\/ctg\/build-owned-runtime\.sh/);
|
|
414
|
+
assert.match(workflow, /GIT_CONFIG_KEY_0:\s*core\.autocrlf[\s\S]*?GIT_CONFIG_VALUE_0:\s*'false'/);
|
|
415
|
+
assert.match(workflow, /git -C opencode config --local core\.autocrlf false/);
|
|
416
|
+
assert.match(workflow, /patch_sha256="\$\(hash_file opencode\/docs\/patches\/opencode-v2\.0\.32-session-ui-combined-v5\.patch\)"/);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test('review-branch prebuild source is exact dispatch SHA; publish validation is default-branch-only', { skip: requiresUbuntuWorkflowShell, timeout: 60000 }, async (t) => {
|
|
420
|
+
const f = await fixture(t);
|
|
421
|
+
const out = path.join(f.directory, 'prebuild-output');
|
|
422
|
+
await writeFile(out, '');
|
|
423
|
+
const env = envFor(f, {
|
|
424
|
+
MODE: 'prebuild',
|
|
425
|
+
SOURCE_COMMIT: runSha,
|
|
426
|
+
GITHUB_REF: 'refs/heads/feature/reviewed-prebuild',
|
|
427
|
+
GITHUB_SHA: runSha,
|
|
428
|
+
GITHUB_OUTPUT: out,
|
|
429
|
+
});
|
|
430
|
+
const allowed = runBlock(validateBlock, f.directory, env);
|
|
431
|
+
assertSuccess(allowed, 'review-branch prebuild validation');
|
|
432
|
+
assert.match(await readFile(out, 'utf8'), new RegExp(`source_commit=${runSha}`));
|
|
433
|
+
|
|
434
|
+
const wrongSha = runBlock(validateBlock, f.directory, { ...env, SOURCE_COMMIT: tagCommit });
|
|
435
|
+
assertFailure(wrongSha, 'prebuild with mismatched dispatch SHA');
|
|
436
|
+
const publishOnReviewBranch = runBlock(validateBlock, f.directory, {
|
|
437
|
+
...envFor(f, { MODE: 'publish', SOURCE_COMMIT: '', GITHUB_REF: 'refs/heads/feature/reviewed-prebuild', GITHUB_OUTPUT: out }),
|
|
438
|
+
});
|
|
439
|
+
assertFailure(publishOnReviewBranch, 'publish validation from non-default branch');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('positive publish verifies five archive bytes and fake-captures exactly seven identical assets', { skip: requiresUbuntuWorkflowShell, timeout: 60000 }, async (t) => {
|
|
443
|
+
const f = await fixture(t);
|
|
444
|
+
const result = await runPublishFixture(f);
|
|
445
|
+
assertSuccess(result.upload, 'fake upload');
|
|
446
|
+
const uploaded = (await readFile(path.join(f.capture, 'uploads.log'), 'utf8')).trim().split('\n').sort();
|
|
447
|
+
assert.deepEqual(uploaded, [
|
|
448
|
+
'opencode-darwin-arm64.tar.gz', 'opencode-darwin-x64.tar.gz',
|
|
449
|
+
'opencode-linux-arm64.tar.gz', 'opencode-linux-x64.tar.gz',
|
|
450
|
+
'opencode-windows-x64.tar.gz', 'runtime-release-SHA256SUMS.txt', 'runtime-release.json',
|
|
451
|
+
]);
|
|
452
|
+
for (const [, , , name] of targets) {
|
|
453
|
+
assert.deepEqual(await readFile(path.join(f.capture, name)), await readFile(path.join(f.candidate, name)));
|
|
454
|
+
}
|
|
455
|
+
assert.deepEqual(await readFile(path.join(f.capture, 'runtime-release.json')), f.manifestBytes);
|
|
456
|
+
const sums = (await readFile(path.join(f.capture, 'runtime-release-SHA256SUMS.txt'), 'utf8')).trim().split('\n');
|
|
457
|
+
assert.equal(sums.length, 6);
|
|
458
|
+
for (const line of sums) {
|
|
459
|
+
const match = line.match(/^([a-f0-9]{64}) (.+)$/);
|
|
460
|
+
assert.ok(match, `checksum line should be sha256sum-formatted: ${line}`);
|
|
461
|
+
assert.equal(match[1], sha256(await readFile(path.join(f.capture, match[2]))), `checksum for ${match[2]}`);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test('tampered candidate archive is rejected before any fake upload', { skip: requiresUbuntuWorkflowShell, timeout: 60000 }, async (t) => {
|
|
466
|
+
const f = await fixture(t);
|
|
467
|
+
const result = await runPublishFixture(f, { tamper: true });
|
|
468
|
+
assert.equal(result.upload, undefined);
|
|
469
|
+
assert.deepEqual(await (await import('node:fs/promises')).readdir(f.capture), []);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('stale annotated tag is rejected by upload recheck before any fake upload', { skip: requiresUbuntuWorkflowShell, timeout: 60000 }, async (t) => {
|
|
473
|
+
const f = await fixture(t);
|
|
474
|
+
const result = await runPublishFixture(f, { tagCommitSha: 'dddddddddddddddddddddddddddddddddddddddd' });
|
|
475
|
+
assertFailure(result.upload, 'stale-tag upload recheck');
|
|
476
|
+
assert.deepEqual(await (await import('node:fs/promises')).readdir(f.capture), []);
|
|
477
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { test } from 'node:test';
|
|
4
|
+
|
|
5
|
+
const workflow = await readFile(new URL('../.github/workflows/release.yml', import.meta.url), 'utf8');
|
|
6
|
+
|
|
7
|
+
test('release workflow requires owned runtime during installed package acceptance', () => {
|
|
8
|
+
const acceptance = workflow.match(/- name: Accept the installed package with its owned runtime required([\s\S]*?)(?=\n - |\n\n publish)/)?.[1];
|
|
9
|
+
assert.ok(acceptance, 'installed acceptance step exists');
|
|
10
|
+
assert.match(acceptance, /CTG_REQUIRE_OWNED_RUNTIME:\s*['"]?1/);
|
|
11
|
+
assert.match(acceptance, /npm run test:native-package/);
|
|
12
|
+
assert.match(acceptance, /CTG_RUNTIME_GITHUB_TOKEN:\s*['"]{2}/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('published release runtime verification explicitly uses the anonymous asset path', () => {
|
|
16
|
+
const verify = workflow.match(/- name: Verify runtime archives against the packaged immutable manifest([\s\S]*?)(?=\n - |\n\s+run:)/)?.[1];
|
|
17
|
+
assert.ok(verify);
|
|
18
|
+
assert.match(verify, /CTG_RUNTIME_GITHUB_TOKEN:\s*['"]{2}/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('npm publication is guarded by release verification and selects latest or next', () => {
|
|
22
|
+
const publish = workflow.match(/ publish-npm:\n([\s\S]*?)(?=\n publish-release-manifest:)/)?.[1];
|
|
23
|
+
assert.ok(publish, 'npm publication job exists');
|
|
24
|
+
assert.match(publish, /needs: verify-release/);
|
|
25
|
+
assert.match(publish, /id-token:\s*write/);
|
|
26
|
+
assert.match(publish, /node-version: 24/);
|
|
27
|
+
assert.match(publish, /registry-url: https:\/\/registry\.npmjs\.org/);
|
|
28
|
+
assert.match(publish, /refs\/tags\/\$\{RELEASE_TAG\}/);
|
|
29
|
+
assert.match(publish, /\[ "\$tag_sha" != "\$EXPECTED_SHA" \]/);
|
|
30
|
+
assert.match(publish, /Set the NPM_TOKEN repository secret/);
|
|
31
|
+
assert.match(publish, /npm view "carthagent@\$\{version\}" version --json/);
|
|
32
|
+
assert.match(publish, /dist_tag=latest/);
|
|
33
|
+
assert.match(publish, /dist_tag=next/);
|
|
34
|
+
assert.match(publish, /npm publish --provenance --access public --tag "\$dist_tag"/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('the final publish step resolves the remote lightweight or annotated tag immediately before POST', () => {
|
|
38
|
+
const publish = workflow.match(/- name: Publish the CLI release manifest to the control plane([\s\S]*)/)?.[1];
|
|
39
|
+
assert.ok(publish, 'publish step exists');
|
|
40
|
+
assert.match(publish, /gh api [^\n]*git\/ref\/tags\/\$\{RELEASE_TAG\}/);
|
|
41
|
+
assert.match(publish, /while \[ "\$tag_type" = tag \]/);
|
|
42
|
+
assert.match(publish, /\[ "\$tag_object" != "\$EXPECTED_SHA" \]/);
|
|
43
|
+
const verifyIndex = publish.indexOf('if [ "$tag_object" != "$EXPECTED_SHA" ]');
|
|
44
|
+
const postIndex = publish.indexOf('curl -fsS -X POST');
|
|
45
|
+
assert.ok(verifyIndex >= 0 && postIndex > verifyIndex, 'tag equality check precedes POST in the same step');
|
|
46
|
+
});
|