mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Fix Vercel recipe failures on stock macOS Bash (#28257)
Guard empty argument-array expansion so the bundled setup recipe runs under Bash 3.2 with unset-variable checks. Preserve exact arguments and cleanup exit behavior; normalize only test fixture line endings. Co-authored-by: outof-place <contact@outofplace.space>
This commit is contained in:
@@ -247,9 +247,11 @@ describe('bundled skill guide generator', () => {
|
||||
const marker = `<!-- bundled-reference: references/${reference} -->`
|
||||
expect(guide.fullMarkdown.split(marker)).toHaveLength(2)
|
||||
expect(guide.fullMarkdown).toContain(
|
||||
await readFile(
|
||||
path.join(projectDir, 'skill-guides', guide.name, 'references', reference),
|
||||
'utf8'
|
||||
normalizeMarkdown(
|
||||
await readFile(
|
||||
path.join(projectDir, 'skill-guides', guide.name, 'references', reference),
|
||||
'utf8'
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -289,33 +291,43 @@ describe('bundled skill guide generator', () => {
|
||||
await expect(verifyArtifacts(first, projectDir)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('generates platform-identical output from CRLF guide sources', async () => {
|
||||
const expected = await buildArtifacts(projectDir)
|
||||
const root = await createFixture()
|
||||
for (const name of CANONICAL_GUIDE_NAMES) {
|
||||
const sourcePath = path.join(root, 'skill-guides', `${name}.md`)
|
||||
const source = await readFile(sourcePath, 'utf8')
|
||||
await writeFile(sourcePath, source.replaceAll('\n', '\r\n'))
|
||||
}
|
||||
for (const name of STUB_TOPICS) {
|
||||
const stubPath = path.join(root, 'skill-stubs', `${name}.md`)
|
||||
const stubSource = await readFile(stubPath, 'utf8')
|
||||
await writeFile(stubPath, stubSource.replaceAll('\n', '\r\n'))
|
||||
}
|
||||
const sharedStubPath = path.join(root, ...SHARED_STUB_SOURCE.split('/'))
|
||||
const sharedStubSource = await readFile(sharedStubPath, 'utf8')
|
||||
await writeFile(sharedStubPath, sharedStubSource.replaceAll('\n', '\r\n'))
|
||||
for (const [guide, reference] of GUIDE_REFERENCE_PATHS) {
|
||||
const referencePath = path.join(root, 'skill-guides', guide, 'references', reference)
|
||||
const source = await readFile(referencePath, 'utf8')
|
||||
await writeFile(referencePath, source.replaceAll('\n', '\r\n'))
|
||||
}
|
||||
it.each([false, true])(
|
||||
'generates platform-identical output from CRLF guide sources (existing CRLF: %s)',
|
||||
async (alreadyCrlf) => {
|
||||
const expected = await buildArtifacts(projectDir)
|
||||
const root = await createFixture()
|
||||
if (alreadyCrlf) {
|
||||
for (const [guide, reference] of GUIDE_REFERENCE_PATHS) {
|
||||
const file = path.join(root, 'skill-guides', guide, 'references', reference)
|
||||
const source = normalizeMarkdown(await readFile(file, 'utf8'))
|
||||
await writeFile(file, source.replaceAll('\n', '\r\n'))
|
||||
}
|
||||
}
|
||||
for (const name of CANONICAL_GUIDE_NAMES) {
|
||||
const sourcePath = path.join(root, 'skill-guides', `${name}.md`)
|
||||
const source = await readFile(sourcePath, 'utf8')
|
||||
await writeFile(sourcePath, normalizeMarkdown(source).replaceAll('\n', '\r\n'))
|
||||
}
|
||||
for (const name of STUB_TOPICS) {
|
||||
const stubPath = path.join(root, 'skill-stubs', `${name}.md`)
|
||||
const stubSource = await readFile(stubPath, 'utf8')
|
||||
await writeFile(stubPath, normalizeMarkdown(stubSource).replaceAll('\n', '\r\n'))
|
||||
}
|
||||
const sharedStubPath = path.join(root, ...SHARED_STUB_SOURCE.split('/'))
|
||||
const sharedStubSource = await readFile(sharedStubPath, 'utf8')
|
||||
await writeFile(sharedStubPath, normalizeMarkdown(sharedStubSource).replaceAll('\n', '\r\n'))
|
||||
for (const [guide, reference] of GUIDE_REFERENCE_PATHS) {
|
||||
const referencePath = path.join(root, 'skill-guides', guide, 'references', reference)
|
||||
const source = await readFile(referencePath, 'utf8')
|
||||
await writeFile(referencePath, normalizeMarkdown(source).replaceAll('\n', '\r\n'))
|
||||
}
|
||||
|
||||
const actual = await buildArtifacts(root)
|
||||
expect(actual.map((artifact) => artifact.content)).toEqual(
|
||||
expected.map((artifact) => artifact.content)
|
||||
)
|
||||
})
|
||||
const actual = await buildArtifacts(root)
|
||||
expect(actual.map((artifact) => artifact.content)).toEqual(
|
||||
expected.map((artifact) => artifact.content)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('pins guide sources, projections, and embedded output to LF in Git', async () => {
|
||||
const attributes = await readFile(path.join(projectDir, '.gitattributes'), 'utf8')
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join, resolve } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
@@ -13,9 +15,9 @@ const vercel = await readFile(resolve(referenceRoot, 'provider-vercel.md'), 'utf
|
||||
const ssh = await readFile(resolve(referenceRoot, 'ssh-host.md'), 'utf8')
|
||||
const cleanup = vercel.match(/```bash\n(cleanup_snapshot\(\) \{[\s\S]*?\n\})\n```/u)?.[1]
|
||||
|
||||
async function runShell(script, env = {}) {
|
||||
async function executeShell(shell, script, env = {}) {
|
||||
try {
|
||||
const output = await run('bash', ['-c', script], {
|
||||
const output = await run(shell, ['-c', script], {
|
||||
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ...env }
|
||||
})
|
||||
return { ...output, code: 0 }
|
||||
@@ -24,56 +26,114 @@ async function runShell(script, env = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('recipe shell examples', () => {
|
||||
it.each(['base', 'auth'])('cleans the %s sandbox on failure and success', async (phase) => {
|
||||
expect(cleanup).toBeDefined()
|
||||
const trap = vercel.match(new RegExp(`trap 'cleanup_snapshot "\\$${phase}"' EXIT`, 'u'))?.[0]
|
||||
expect(trap).toBeDefined()
|
||||
expect(vercel.indexOf(trap)).toBeLessThan(
|
||||
vercel.indexOf(`vercel sandbox create --name "$${phase}"`)
|
||||
)
|
||||
for (const exitCode of [0, 7]) {
|
||||
const result = await runShell(`set -euo pipefail
|
||||
// Stock macOS Bash must run even when PATH starts with a newer Bash.
|
||||
const shells = existsSync('/bin/bash') ? ['/bin/bash', 'bash'] : ['bash']
|
||||
|
||||
describe.skipIf(process.platform === 'win32').each(shells)(
|
||||
'recipe shell examples (%s)',
|
||||
(shell) => {
|
||||
const runShell = (script, env) => executeShell(shell, script, env)
|
||||
|
||||
it.each(['base', 'auth'])('cleans the %s sandbox on failure and success', async (phase) => {
|
||||
expect(cleanup).toBeDefined()
|
||||
const trap = vercel.match(new RegExp(`trap 'cleanup_snapshot "\\$${phase}"' EXIT`, 'u'))?.[0]
|
||||
expect(trap).toBeDefined()
|
||||
expect(vercel.indexOf(trap)).toBeLessThan(
|
||||
vercel.indexOf(`vercel sandbox create --name "$${phase}"`)
|
||||
)
|
||||
for (const exitCode of [0, 7]) {
|
||||
const result = await runShell(`set -euo pipefail
|
||||
${cleanup}
|
||||
vercel_args=(--scope test-scope)
|
||||
${phase}=unique-test-sandbox
|
||||
vercel() { printf '%s\\n' "$@"; }
|
||||
${trap}
|
||||
exit ${exitCode}`)
|
||||
expect(result.code).toBe(exitCode)
|
||||
expect(result.stderr).toBe('sandbox\nremove\nunique-test-sandbox\n--scope\ntest-scope\n')
|
||||
}
|
||||
})
|
||||
expect(result.code).toBe(exitCode)
|
||||
expect(result.stderr).toBe('sandbox\nremove\nunique-test-sandbox\n--scope\ntest-scope\n')
|
||||
}
|
||||
})
|
||||
|
||||
it('reports failed cleanup even after an otherwise successful snapshot', async () => {
|
||||
const result = await runShell(`set -euo pipefail
|
||||
it('reports failed cleanup even after an otherwise successful snapshot', async () => {
|
||||
const result = await runShell(`set -euo pipefail
|
||||
${cleanup}
|
||||
vercel_args=()
|
||||
vercel() { return 9; }
|
||||
trap 'cleanup_snapshot unique-test-sandbox' EXIT
|
||||
exit 0`)
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.stderr).toContain('Sandbox cleanup failed for unique-test-sandbox')
|
||||
})
|
||||
expect(result.code).toBe(1)
|
||||
expect(result.stderr).toContain('Sandbox cleanup failed for unique-test-sandbox')
|
||||
})
|
||||
|
||||
it('disables Git prompts when the Vercel token is absent', async () => {
|
||||
const prefix = vercel.match(
|
||||
/-- bash -lc 'set -euo pipefail; cd "\$ORCA_PROJECT_ROOT"; \\\n([\s\S]*?) git fetch/u
|
||||
)?.[1]
|
||||
expect(prefix).toBeDefined()
|
||||
const result = await runShell(
|
||||
`set -euo pipefail\nunset GH_TOKEN\n${prefix}\nprintf '%s' "$GIT_TERMINAL_PROMPT"`
|
||||
it.each([
|
||||
{ scope: '', project: '' },
|
||||
{ scope: 'scope with spaces *', project: '' },
|
||||
{ scope: '', project: 'project with spaces ?' },
|
||||
{ scope: 'scope with spaces *', project: 'project with spaces ?' }
|
||||
])(
|
||||
'runs the full create recipe with scope "$scope" and project "$project"',
|
||||
async ({ scope, project }) => {
|
||||
const result = await runCreateRecipe(shell, {
|
||||
VERCEL_SCOPE: scope,
|
||||
VERCEL_PROJECT: project
|
||||
})
|
||||
expect(result.code).toBe(0)
|
||||
const output = JSON.parse(result.stdout)
|
||||
expect(output.userData).toMatchObject({
|
||||
provider: 'vercel-sandbox',
|
||||
resourceId: 'orca-fixture-123',
|
||||
snapshotId: 'snap_fixture'
|
||||
})
|
||||
expect(result.calls.map((args) => args.slice(0, 2))).toEqual([
|
||||
['sandbox', 'create'],
|
||||
['sandbox', 'exec'],
|
||||
['sandbox', 'exec']
|
||||
])
|
||||
const expectedFlags = [
|
||||
...(scope ? ['--scope', scope] : []),
|
||||
...(project ? ['--project', project] : [])
|
||||
]
|
||||
for (const args of result.calls) {
|
||||
const start = args.findIndex((arg) => arg === '--scope' || arg === '--project')
|
||||
if (expectedFlags.length) {
|
||||
expect(args.slice(start, start + expectedFlags.length)).toEqual(expectedFlags)
|
||||
} else {
|
||||
expect(start).toBe(-1)
|
||||
}
|
||||
expect(args).not.toContain('')
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stdout).toBe('0')
|
||||
})
|
||||
|
||||
it('uses host credentials and refuses unverified SSH hosts without forwarding tokens', async () => {
|
||||
const script = ssh.match(/```bash\n(#!\/usr\/bin\/env bash[\s\S]*?)\n```/u)?.[1]
|
||||
expect(script).toBeDefined()
|
||||
const sync = script.slice(0, script.indexOf('# 2. print'))
|
||||
const result = await runShell(
|
||||
`ssh() { printf '%s\\n' "$@"; }
|
||||
it('removes a failed create with no optional flags', async () => {
|
||||
const result = await runCreateRecipe(shell, { VERCEL_CREATE_EXIT: '7' })
|
||||
expect(result.code).toBe(7)
|
||||
expect(result.calls.map((args) => args.slice(0, 2))).toEqual([
|
||||
['sandbox', 'create'],
|
||||
['sandbox', 'remove']
|
||||
])
|
||||
expect(result.calls[1]).toEqual(['sandbox', 'remove', 'orca-fixture-123'])
|
||||
expect(result.stderr).not.toContain('unbound variable')
|
||||
})
|
||||
|
||||
it('disables Git prompts when the Vercel token is absent', async () => {
|
||||
const prefix = vercel.match(
|
||||
/-- bash -lc 'set -euo pipefail; cd "\$ORCA_PROJECT_ROOT"; \\\n([\s\S]*?) git fetch/u
|
||||
)?.[1]
|
||||
expect(prefix).toBeDefined()
|
||||
const result = await runShell(
|
||||
`set -euo pipefail\nunset GH_TOKEN\n${prefix}\nprintf '%s' "$GIT_TERMINAL_PROMPT"`
|
||||
)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stdout).toBe('0')
|
||||
})
|
||||
|
||||
it('uses host credentials and refuses unverified SSH hosts without forwarding tokens', async () => {
|
||||
const script = ssh.match(/```bash\n(#!\/usr\/bin\/env bash[\s\S]*?)\n```/u)?.[1]
|
||||
expect(script).toBeDefined()
|
||||
const sync = script.slice(0, script.indexOf('# 2. print'))
|
||||
const result = await runShell(
|
||||
`ssh() { printf '%s\\n' "$@"; }
|
||||
ssh_username=worker
|
||||
host=example.test
|
||||
ssh_port=2222
|
||||
@@ -81,13 +141,65 @@ project_root='/remote/path with spaces'
|
||||
repo_url=https://example.test/org/repo.git
|
||||
repo_ref=main
|
||||
${sync}`,
|
||||
{ GH_TOKEN: 'test-token-must-not-be-forwarded' }
|
||||
{ GH_TOKEN: 'test-token-must-not-be-forwarded' }
|
||||
)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stderr).toContain('StrictHostKeyChecking=yes')
|
||||
expect(result.stderr).toContain('BatchMode=yes')
|
||||
expect(result.stderr).not.toContain('test-token-must-not-be-forwarded')
|
||||
expect(result.stderr).not.toContain('GH_TOKEN=')
|
||||
expect(script).toContain('export GIT_TERMINAL_PROMPT=0')
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
async function runCreateRecipe(shell, overrides) {
|
||||
const script = vercel.match(/```bash\n(#!\/usr\/bin\/env bash[\s\S]*?)\n```/u)?.[1]
|
||||
expect(script).toBeDefined()
|
||||
const directory = await mkdtemp(join(tmpdir(), 'orca-vercel-recipe-'))
|
||||
const bin = join(directory, 'bin')
|
||||
const log = join(directory, 'calls.jsonl')
|
||||
try {
|
||||
await mkdir(bin)
|
||||
const command = join(bin, 'vercel')
|
||||
await writeFile(
|
||||
command,
|
||||
`#!/usr/bin/env node
|
||||
const { appendFileSync } = require('node:fs')
|
||||
const args = process.argv.slice(2)
|
||||
appendFileSync(process.env.VERCEL_FIXTURE_LOG, JSON.stringify(args) + '\\n')
|
||||
if (args[1] === 'create') {
|
||||
if (process.env.VERCEL_CREATE_EXIT) process.exit(Number(process.env.VERCEL_CREATE_EXIT))
|
||||
console.log('https://fixture.vercel.run')
|
||||
} else if (args[1] === 'exec' && args.includes('60s')) {
|
||||
console.log(JSON.stringify({ userData: {} }))
|
||||
}
|
||||
`
|
||||
)
|
||||
expect(result.code).toBe(0)
|
||||
expect(result.stderr).toContain('StrictHostKeyChecking=yes')
|
||||
expect(result.stderr).toContain('BatchMode=yes')
|
||||
expect(result.stderr).not.toContain('test-token-must-not-be-forwarded')
|
||||
expect(result.stderr).not.toContain('GH_TOKEN=')
|
||||
expect(script).toContain('export GIT_TERMINAL_PROMPT=0')
|
||||
})
|
||||
})
|
||||
await chmod(command, 0o755)
|
||||
const result = await executeShell(
|
||||
shell,
|
||||
`scope="$VERCEL_SCOPE"; project="$VERCEL_PROJECT"
|
||||
snapshot_id=snap_fixture; port=6768; repo_url=https://example.test/repo.git; repo_ref=main; project_root='/fixture repo'
|
||||
${script}`,
|
||||
{
|
||||
PATH: `${bin}${delimiter}${process.env.PATH}`,
|
||||
VERCEL_FIXTURE_LOG: log,
|
||||
VERCEL_SCOPE: '',
|
||||
VERCEL_PROJECT: '',
|
||||
VERCEL_CREATE_EXIT: '',
|
||||
GH_TOKEN: 'fixture-token',
|
||||
ORCA_RECIPE_ID: 'fixture',
|
||||
ORCA_VM_INSTANCE_ID: '123',
|
||||
...overrides
|
||||
}
|
||||
)
|
||||
const calls = (await readFile(log, 'utf8'))
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => JSON.parse(line))
|
||||
return { ...result, calls }
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,14 @@ The base and auth excerpts each belong to one `set -euo pipefail` script. Includ
|
||||
in both scripts and arm the trap before creating their temporary sandbox. Keep it armed through
|
||||
verification, snapshot creation, and writing state; cleanup failure must remain visible.
|
||||
|
||||
Use the guarded array expansion below so an empty `vercel_args` array works under `set -u`
|
||||
on stock macOS Bash 3.2 while preserving each configured argument.
|
||||
|
||||
```bash
|
||||
cleanup_snapshot() {
|
||||
snapshot_exit=$?
|
||||
trap - EXIT
|
||||
if ! vercel sandbox remove "$1" "${vercel_args[@]}" >&2; then
|
||||
if ! vercel sandbox remove "$1" ${vercel_args[@]+"${vercel_args[@]}"} >&2; then
|
||||
echo "Sandbox cleanup failed for $1; inspect and remove it before continuing" >&2
|
||||
snapshot_exit=1
|
||||
fi
|
||||
@@ -36,14 +39,14 @@ Provision, install tools and clone, build headless, then snapshot.
|
||||
# provision a fresh build sandbox (retain a couple of snapshots)
|
||||
trap 'cleanup_snapshot "$base"' EXIT
|
||||
vercel sandbox create --name "$base" --runtime node24 --timeout 30m --vcpus 4 --publish-port "$port" \
|
||||
--snapshot-expiration 30d --keep-last-snapshots 2 "${vercel_args[@]}" >&2
|
||||
--snapshot-expiration 30d --keep-last-snapshots 2 ${vercel_args[@]+"${vercel_args[@]}"} >&2
|
||||
# remote build (long timeout): install pkgs+gh+pnpm+agent CLI, clone with GIT_ASKPASS (the helper's
|
||||
# \$1/\$GH_TOKEN escaping is load-bearing — see the guide's Credentials section — then
|
||||
# `rm -f /tmp/askpass.sh`), write the headless main-only build config (drop the renderer), dev setup,
|
||||
# build CLI + headless main, smoke-check
|
||||
vercel sandbox exec "$base" "${vercel_args[@]}" --timeout 25m --env "GH_TOKEN=$gh_token" … -- bash -lc '…build…' >&2
|
||||
vercel sandbox exec "$base" ${vercel_args[@]+"${vercel_args[@]}"} --timeout 25m --env "GH_TOKEN=$gh_token" … -- bash -lc '…build…' >&2
|
||||
# snapshot the STOPPED sandbox and parse the id from CLI output (fail if unparseable)
|
||||
out="$(vercel sandbox snapshot "$base" --stop --expiration 30d "${vercel_args[@]}" 2>&1)"; printf '%s\n' "$out" >&2
|
||||
out="$(vercel sandbox snapshot "$base" --stop --expiration 30d ${vercel_args[@]+"${vercel_args[@]}"} 2>&1)"; printf '%s\n' "$out" >&2
|
||||
snapshot_id="$(printf '%s\n' "$out" | sed -nE 's/.*(snap_[A-Za-z0-9]+).*/\1/p' | tail -1)"
|
||||
[ -n "$snapshot_id" ] || { echo "snapshot id missing" >&2; exit 1; }
|
||||
# merge { baseName, snapshotId, scope, project, port, repoUrl, repoRef, projectRoot } into state; print state JSON
|
||||
@@ -56,16 +59,16 @@ substitute the user's chosen agent's login and status verbs.
|
||||
|
||||
```bash
|
||||
trap 'cleanup_snapshot "$auth"' EXIT
|
||||
vercel sandbox create --name "$auth" --snapshot "$snapshot_id" --timeout 30m --publish-port "$port" "${vercel_args[@]}" >&2
|
||||
vercel sandbox create --name "$auth" --snapshot "$snapshot_id" --timeout 30m --publish-port "$port" ${vercel_args[@]+"${vercel_args[@]}"} >&2
|
||||
# The USER runs this in their own terminal and completes the URL/code on the HOST.
|
||||
vercel sandbox exec --interactive --tty "$auth" "${vercel_args[@]}" -- bash -lc 'codex login --device-auth'
|
||||
vercel sandbox exec --interactive --tty "$auth" ${vercel_args[@]+"${vercel_args[@]}"} -- bash -lc 'codex login --device-auth'
|
||||
```
|
||||
|
||||
Verify by exit code. The remote command prints a sentinel instead of relying on the exit code,
|
||||
because a provider CLI may not propagate remote exit codes:
|
||||
|
||||
```bash
|
||||
verdict="$(vercel sandbox exec "$auth" "${vercel_args[@]}" --timeout 30s \
|
||||
verdict="$(vercel sandbox exec "$auth" ${vercel_args[@]+"${vercel_args[@]}"} --timeout 30s \
|
||||
-- bash -lc 'if codex login status >/dev/null 2>&1; then echo ORCA_AGENT_LOGGED_IN; else echo ORCA_AGENT_LOGGED_OUT; fi')"
|
||||
case "$verdict" in
|
||||
*ORCA_AGENT_LOGGED_IN*) ;;
|
||||
@@ -78,7 +81,7 @@ stderr folded in and match the agent's exact success line. Match a variable, not
|
||||
provider process cannot take SIGPIPE:
|
||||
|
||||
```bash
|
||||
status="$(vercel sandbox exec "$auth" "${vercel_args[@]}" --timeout 30s -- bash -lc 'codex login status 2>&1')"
|
||||
status="$(vercel sandbox exec "$auth" ${vercel_args[@]+"${vercel_args[@]}"} --timeout 30s -- bash -lc 'codex login status 2>&1')"
|
||||
grep -Eq 'Logged in using ChatGPT|Logged in via device' <<<"$status" \
|
||||
|| { echo "agent not logged in; not snapshotting" >&2; exit 1; }
|
||||
```
|
||||
@@ -86,7 +89,7 @@ grep -Eq 'Logged in using ChatGPT|Logged in via device' <<<"$status" \
|
||||
Then re-snapshot and record the new id:
|
||||
|
||||
```bash
|
||||
out="$(vercel sandbox snapshot "$auth" --stop --expiration 30d "${vercel_args[@]}" 2>&1)"; printf '%s\n' "$out" >&2
|
||||
out="$(vercel sandbox snapshot "$auth" --stop --expiration 30d ${vercel_args[@]+"${vercel_args[@]}"} 2>&1)"; printf '%s\n' "$out" >&2
|
||||
new_id="$(printf '%s\n' "$out" | sed -nE 's/.*(snap_[A-Za-z0-9]+).*/\1/p' | tail -1)"
|
||||
[ -n "$new_id" ] || { echo "authenticated snapshot id missing" >&2; exit 1; }
|
||||
# overwrite state.snapshotId = new_id, record authSourceSnapshotId = snapshot_id; remove the auth sandbox
|
||||
@@ -109,19 +112,19 @@ max_recipe_id_length=$((128 - ${#instance_id} - 6)) # Preserve the unique insta
|
||||
name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"
|
||||
|
||||
# Arm cleanup BEFORE create so a failing create can't leak a half-built paid sandbox.
|
||||
cleanup_on_error() { [ "$?" -ne 0 ] && vercel sandbox remove "$name" "${vercel_args[@]}" >/dev/null 2>&1 || true; }
|
||||
cleanup_on_error() { [ "$?" -ne 0 ] && vercel sandbox remove "$name" ${vercel_args[@]+"${vercel_args[@]}"} >/dev/null 2>&1 || true; }
|
||||
trap cleanup_on_error EXIT
|
||||
|
||||
# 1. boot from the authenticated snapshot, publish the serve port
|
||||
create_output="$(vercel sandbox create --name "$name" --snapshot "$snapshot_id" \
|
||||
--timeout 30m --publish-port "$port" "${vercel_args[@]}" 2>&1)"; printf '%s\n' "$create_output" >&2
|
||||
--timeout 30m --publish-port "$port" ${vercel_args[@]+"${vercel_args[@]}"} 2>&1)"; printf '%s\n' "$create_output" >&2
|
||||
# Vercel prints the published https URL; derive the external wss:// pairing address from it
|
||||
public_url="$(printf '%s\n' "$create_output" | sed -nE 's#.*(https://[^[:space:]]+\.vercel\.run).*#\1#p' | head -1)"
|
||||
[ -n "$public_url" ] || { echo "no published URL in create output" >&2; exit 1; }
|
||||
pairing_ws="${public_url/https:\/\//wss://}"
|
||||
|
||||
# 2. (remote) ensure the repo is at the right commit; rebuild only if the commit changed (cache marker)
|
||||
vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 20m \
|
||||
vercel sandbox exec "$name" ${vercel_args[@]+"${vercel_args[@]}"} --timeout 20m \
|
||||
--env "GH_TOKEN=$gh_token" --env "ORCA_PROJECT_ROOT=$project_root" \
|
||||
--env "ORCA_REPO_URL=$repo_url" --env "ORCA_REPO_REF=$repo_ref" \
|
||||
-- bash -lc 'set -euo pipefail; cd "$ORCA_PROJECT_ROOT"; \
|
||||
@@ -139,7 +142,7 @@ vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 20m \
|
||||
printf "%s" "$c" > .orca-built; }' >&2
|
||||
|
||||
# 3. (remote) start orca serve in the background, writing recipe JSON to a file; poll until it parses
|
||||
recipe_json="$(vercel sandbox exec "$name" "${vercel_args[@]}" --timeout 60s \
|
||||
recipe_json="$(vercel sandbox exec "$name" ${vercel_args[@]+"${vercel_args[@]}"} --timeout 60s \
|
||||
--env "ORCA_PORT=$port" --env "ORCA_PROJECT_ROOT=$project_root" --env "ORCA_PAIRING_ADDRESS=$pairing_ws" \
|
||||
-- bash -lc 'set -euo pipefail; cd "$ORCA_PROJECT_ROOT"; rm -f /tmp/orca-recipe.json /tmp/orca-serve.log; \
|
||||
nohup pnpm exec orca-dev serve --port "$ORCA_PORT" --project-root "$ORCA_PROJECT_ROOT" \
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user