mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
build: centralize the pinned opencode version (#1075)
Keep OpenCode version selection predictable by reading a single repo-wide constant and packaging that pin into orchestrator builds. Remove env and latest-release fallbacks so desktop, workers, snapshots, and CI stay aligned. Co-authored-by: Omar McAdam <omar@OpenWork-Studio.localdomain>
This commit is contained in:
68
.github/workflows/alpha-macos-aarch64.yml
vendored
68
.github/workflows/alpha-macos-aarch64.yml
vendored
@@ -21,7 +21,6 @@ jobs:
|
||||
|
||||
env:
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -85,80 +84,27 @@ jobs:
|
||||
- name: Resolve OpenCode version
|
||||
id: opencode-version
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
node <<'NODE' >> "$GITHUB_OUTPUT"
|
||||
const fs = require('fs');
|
||||
const repo = (process.env.OPENCODE_GITHUB_REPO || 'anomalyco/opencode').trim() || 'anomalyco/opencode';
|
||||
|
||||
async function resolveLatest() {
|
||||
const token = (process.env.GITHUB_TOKEN || '').trim();
|
||||
const headers = {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'openwork-ci',
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const tag = (typeof data.tag_name === 'string' ? data.tag_name : '').trim();
|
||||
const version = (tag.startsWith('v') ? tag.slice(1) : tag).trim();
|
||||
if (version) return version;
|
||||
const parsed = JSON.parse(fs.readFileSync('./constants.json', 'utf8'));
|
||||
const version = String(parsed.opencodeVersion || '').replace(/^v/, '').trim();
|
||||
if (!version) {
|
||||
throw new Error('Pinned OpenCode version is missing from constants.json');
|
||||
}
|
||||
if (res.status !== 403) {
|
||||
throw new Error(`Failed to resolve latest OpenCode version (HTTP ${res.status})`);
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
const web = await fetch(`https://github.com/${repo}/releases/latest`, {
|
||||
headers: { 'User-Agent': 'openwork-ci' },
|
||||
redirect: 'follow',
|
||||
});
|
||||
const url = web && web.url ? String(web.url) : '';
|
||||
const match = url.match(/\/tag\/v([^/?#]+)/);
|
||||
if (!match) throw new Error('Failed to resolve latest OpenCode version (web redirect).');
|
||||
return match[1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const pkg = JSON.parse(fs.readFileSync('./apps/desktop/package.json', 'utf8'));
|
||||
const configuredRaw = (process.env.OPENCODE_VERSION || pkg.opencodeVersion || '').toString().trim();
|
||||
if (configuredRaw && configuredRaw.toLowerCase() !== 'latest') {
|
||||
const resolved = (configuredRaw.startsWith('v') ? configuredRaw.slice(1) : configuredRaw).trim();
|
||||
if (process.env.GITHUB_ENV) {
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${resolved}\n`);
|
||||
}
|
||||
console.log('version=' + resolved);
|
||||
return;
|
||||
}
|
||||
const latest = await resolveLatest();
|
||||
if (process.env.GITHUB_ENV) {
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${latest}\n`);
|
||||
}
|
||||
console.log('version=' + latest);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
console.log('version=' + version);
|
||||
NODE
|
||||
|
||||
- name: Download OpenCode sidecar
|
||||
shell: bash
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
|
||||
PINNED_OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${OPENCODE_GITHUB_REPO:-anomalyco/opencode}"
|
||||
opencode_asset="opencode-darwin-arm64.zip"
|
||||
url="https://github.com/${repo}/releases/download/v${OPENCODE_VERSION}/${opencode_asset}"
|
||||
url="https://github.com/${repo}/releases/download/v${PINNED_OPENCODE_VERSION}/${opencode_asset}"
|
||||
tmp_dir="$RUNNER_TEMP/opencode"
|
||||
extract_dir="$tmp_dir/extracted"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
31
.github/workflows/build-desktop.yml
vendored
31
.github/workflows/build-desktop.yml
vendored
@@ -71,45 +71,18 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${OPENCODE_GITHUB_REPO:-anomalyco/opencode}"
|
||||
version="${OPENCODE_VERSION:-}"
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
version="$(node -p "require('./apps/desktop/package.json').opencodeVersion || ''" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
version="$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")"
|
||||
version="$(echo "$version" | tr -d '\r\n' | sed 's/^v//')"
|
||||
if [ -z "$version" ] || [ "$version" = "latest" ]; then
|
||||
latest=""
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
latest=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/${repo}/releases/latest" \
|
||||
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
else
|
||||
latest=$(curl -fsSL \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/${repo}/releases/latest" \
|
||||
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
fi
|
||||
if [ -n "$latest" ]; then
|
||||
version="$latest"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Unable to resolve OpenCode version (set OPENCODE_VERSION to pin)." >&2
|
||||
echo "Unable to resolve OpenCode version from constants.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reuse this resolved version for subsequent steps (e.g. prepare-sidecar).
|
||||
echo "OPENCODE_VERSION=$version" >> "$GITHUB_ENV"
|
||||
|
||||
opencode_asset="opencode-linux-x64-baseline.tar.gz"
|
||||
url="https://github.com/${repo}/releases/download/v${version}/${opencode_asset}"
|
||||
tmp_dir="$RUNNER_TEMP/opencode"
|
||||
|
||||
29
.github/workflows/ci-tests.yml
vendored
29
.github/workflows/ci-tests.yml
vendored
@@ -39,40 +39,15 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
repo="${OPENCODE_GITHUB_REPO:-anomalyco/opencode}"
|
||||
version="${OPENCODE_VERSION:-}"
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
version="$(node -p "require('./apps/desktop/package.json').opencodeVersion || ''" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
version="$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")"
|
||||
version="$(echo "$version" | tr -d '\r\n' | sed 's/^v//')"
|
||||
|
||||
if [ -z "$version" ] || [ "$version" = "latest" ]; then
|
||||
latest=""
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
latest=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/${repo}/releases/latest" \
|
||||
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
else
|
||||
latest=$(curl -fsSL \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"https://api.github.com/repos/${repo}/releases/latest" \
|
||||
| sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
|
||||
fi
|
||||
if [ -n "$latest" ]; then
|
||||
version="$latest"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Unable to resolve OpenCode version (set OPENCODE_VERSION to pin)." >&2
|
||||
echo "Unable to resolve OpenCode version from constants.json." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
8
.github/workflows/opencode-agents.yml
vendored
8
.github/workflows/opencode-agents.yml
vendored
@@ -20,7 +20,9 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
run: |
|
||||
version="$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")"
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version "$version" --no-modify-path
|
||||
|
||||
- name: Triage issue
|
||||
env:
|
||||
@@ -48,7 +50,9 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
run: |
|
||||
version="$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")"
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version "$version" --no-modify-path
|
||||
|
||||
- name: Build prompt
|
||||
env:
|
||||
|
||||
72
.github/workflows/prerelease.yml
vendored
72
.github/workflows/prerelease.yml
vendored
@@ -85,7 +85,6 @@ jobs:
|
||||
RELEASE_BODY: ${{ needs.prepare-release.outputs.release_body }}
|
||||
MACOS_NOTARIZE: ${{ vars.MACOS_NOTARIZE || 'false' }}
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -172,78 +171,21 @@ jobs:
|
||||
- name: Resolve OpenCode version
|
||||
id: opencode-version
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
node <<'NODE' >> "$GITHUB_OUTPUT"
|
||||
const fs = require('fs');
|
||||
const repo = (process.env.OPENCODE_GITHUB_REPO || 'anomalyco/opencode').trim() || 'anomalyco/opencode';
|
||||
|
||||
async function resolveLatest() {
|
||||
const token = (process.env.GITHUB_TOKEN || '').trim();
|
||||
const headers = {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'openwork-ci',
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
// Prefer API, but fall back to the web "latest" redirect if rate-limited (403) or otherwise blocked.
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const tag = (typeof data.tag_name === 'string' ? data.tag_name : '').trim();
|
||||
let v = tag.startsWith('v') ? tag.slice(1) : tag;
|
||||
v = v.trim();
|
||||
if (v) return v;
|
||||
const parsed = JSON.parse(fs.readFileSync('./constants.json', 'utf8'));
|
||||
const version = String(parsed.opencodeVersion || '').replace(/^v/, '').trim();
|
||||
if (!version) {
|
||||
throw new Error('Pinned OpenCode version is missing from constants.json');
|
||||
}
|
||||
if (res.status !== 403) {
|
||||
throw new Error(`Failed to resolve latest OpenCode version (HTTP ${res.status})`);
|
||||
}
|
||||
} catch {
|
||||
// continue to fallback
|
||||
}
|
||||
|
||||
const web = await fetch(`https://github.com/${repo}/releases/latest`, {
|
||||
headers: { 'User-Agent': 'openwork-ci' },
|
||||
redirect: 'follow',
|
||||
});
|
||||
const url = (web && web.url) ? String(web.url) : '';
|
||||
const match = url.match(/\/tag\/v([^/?#]+)/);
|
||||
if (!match) throw new Error('Failed to resolve latest OpenCode version (web redirect).');
|
||||
return match[1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const pkg = JSON.parse(fs.readFileSync('./apps/desktop/package.json', 'utf8'));
|
||||
const configuredRaw = (process.env.OPENCODE_VERSION || pkg.opencodeVersion || '').toString().trim();
|
||||
if (configuredRaw && configuredRaw.toLowerCase() !== 'latest') {
|
||||
const normalized = configuredRaw.startsWith('v') ? configuredRaw.slice(1) : configuredRaw;
|
||||
const resolved = normalized.trim();
|
||||
if (process.env.GITHUB_ENV) {
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${resolved}\n`);
|
||||
}
|
||||
console.log('version=' + resolved);
|
||||
return;
|
||||
}
|
||||
const latest = await resolveLatest();
|
||||
if (process.env.GITHUB_ENV) {
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${latest}\n`);
|
||||
}
|
||||
console.log('version=' + latest);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
console.log('version=' + version);
|
||||
NODE
|
||||
|
||||
- name: Download OpenCode sidecar
|
||||
shell: bash
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
|
||||
PINNED_OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -270,7 +212,7 @@ jobs:
|
||||
esac
|
||||
|
||||
repo="${OPENCODE_GITHUB_REPO:-anomalyco/opencode}"
|
||||
url="https://github.com/${repo}/releases/download/v${OPENCODE_VERSION}/${opencode_asset}"
|
||||
url="https://github.com/${repo}/releases/download/v${PINNED_OPENCODE_VERSION}/${opencode_asset}"
|
||||
tmp_dir="$RUNNER_TEMP/opencode"
|
||||
extract_dir="$tmp_dir/extracted"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
74
.github/workflows/release-macos-aarch64.yml
vendored
74
.github/workflows/release-macos-aarch64.yml
vendored
@@ -254,7 +254,6 @@ jobs:
|
||||
MACOS_NOTARIZE: ${{ needs.resolve-release.outputs.notarize }}
|
||||
# Ensure Tauri's beforeBuildCommand (prepare:sidecar) uses our fork.
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -373,80 +372,21 @@ jobs:
|
||||
- name: Resolve OpenCode version
|
||||
id: opencode-version
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
OPENCODE_VERSION: ${{ vars.OPENCODE_VERSION || '1.2.20' }}
|
||||
run: |
|
||||
node <<'NODE' >> "$GITHUB_OUTPUT"
|
||||
const fs = require('fs');
|
||||
const repo = (process.env.OPENCODE_GITHUB_REPO || 'anomalyco/opencode').trim() || 'anomalyco/opencode';
|
||||
|
||||
async function resolveLatest() {
|
||||
const token = (process.env.GITHUB_TOKEN || '').trim();
|
||||
const headers = {
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
'User-Agent': 'openwork-ci',
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
// Prefer API, but fall back to the web "latest" redirect if rate-limited (403) or otherwise blocked.
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const tag = (typeof data.tag_name === 'string' ? data.tag_name : '').trim();
|
||||
let v = tag.startsWith('v') ? tag.slice(1) : tag;
|
||||
v = v.trim();
|
||||
if (v) return v;
|
||||
const parsed = JSON.parse(fs.readFileSync('./constants.json', 'utf8'));
|
||||
const version = String(parsed.opencodeVersion || '').replace(/^v/, '').trim();
|
||||
if (!version) {
|
||||
throw new Error('Pinned OpenCode version is missing from constants.json');
|
||||
}
|
||||
if (res.status !== 403) {
|
||||
throw new Error(`Failed to resolve latest OpenCode version (HTTP ${res.status})`);
|
||||
}
|
||||
} catch {
|
||||
// continue to fallback
|
||||
}
|
||||
|
||||
const web = await fetch(`https://github.com/${repo}/releases/latest`, {
|
||||
headers: { 'User-Agent': 'openwork-ci' },
|
||||
redirect: 'follow',
|
||||
});
|
||||
const url = (web && web.url) ? String(web.url) : '';
|
||||
const match = url.match(/\/tag\/v([^/?#]+)/);
|
||||
if (!match) throw new Error('Failed to resolve latest OpenCode version (web redirect).');
|
||||
return match[1];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const pkg = JSON.parse(fs.readFileSync('./apps/desktop/package.json', 'utf8'));
|
||||
const configuredRaw = (process.env.OPENCODE_VERSION || pkg.opencodeVersion || '').toString().trim();
|
||||
if (configuredRaw && configuredRaw.toLowerCase() !== 'latest') {
|
||||
const normalized = configuredRaw.startsWith('v') ? configuredRaw.slice(1) : configuredRaw;
|
||||
const resolved = normalized.trim();
|
||||
if (process.env.GITHUB_ENV) {
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${resolved}\n`);
|
||||
}
|
||||
console.log('version=' + resolved);
|
||||
return;
|
||||
}
|
||||
const latest = await resolveLatest();
|
||||
if (process.env.GITHUB_ENV) {
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `OPENCODE_VERSION=${latest}\n`);
|
||||
}
|
||||
console.log('version=' + latest);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
console.log('version=' + version);
|
||||
NODE
|
||||
|
||||
- name: Download OpenCode sidecar
|
||||
shell: bash
|
||||
env:
|
||||
OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
|
||||
PINNED_OPENCODE_VERSION: ${{ steps.opencode-version.outputs.version }}
|
||||
OPENCODE_GITHUB_REPO: ${{ vars.OPENCODE_GITHUB_REPO || 'anomalyco/opencode' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -474,7 +414,7 @@ jobs:
|
||||
esac
|
||||
|
||||
repo="${OPENCODE_GITHUB_REPO:-anomalyco/opencode}"
|
||||
url="https://github.com/${repo}/releases/download/v${OPENCODE_VERSION}/${opencode_asset}"
|
||||
url="https://github.com/${repo}/releases/download/v${PINNED_OPENCODE_VERSION}/${opencode_asset}"
|
||||
tmp_dir="$RUNNER_TEMP/opencode"
|
||||
extract_dir="$tmp_dir/extracted"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
@@ -141,7 +141,7 @@ All repo `dev` entrypoints now opt into the same dev-mode isolation so local tes
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed webkit2gtk-4.1
|
||||
yay -s opencode # Releases version
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version "$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")" --no-modify-path
|
||||
```
|
||||
|
||||
## Architecture (high-level)
|
||||
|
||||
@@ -111,7 +111,7 @@ pnpm dev:ui
|
||||
### Arch 用户:
|
||||
|
||||
```bash
|
||||
yay -s opencode # 已发布版本
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version "$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")" --no-modify-path
|
||||
```
|
||||
|
||||
## 架构(高级)
|
||||
|
||||
@@ -111,7 +111,7 @@ pnpm dev:ui
|
||||
### Arch 用戶:
|
||||
|
||||
```bash
|
||||
yay -s opencode # 已發布版本
|
||||
curl -fsSL https://opencode.ai/install | bash -s -- --version "$(node -e "const fs=require('fs'); const parsed=JSON.parse(fs.readFileSync('constants.json','utf8')); process.stdout.write(String(parsed.opencodeVersion||'').trim().replace(/^v/,''));")" --no-modify-path
|
||||
```
|
||||
|
||||
## 架構(高級)
|
||||
|
||||
@@ -2559,8 +2559,8 @@ export function createWorkspaceStore(options: {
|
||||
if (!result.found) {
|
||||
options.setError(
|
||||
options.isWindowsPlatform()
|
||||
? "OpenCode CLI not found. Install OpenCode for Windows or bundle opencode.exe with OpenWork, then restart. If it is installed, ensure `opencode.exe` is on PATH (try `opencode --version` in PowerShell)."
|
||||
: "OpenCode CLI not found. Install with `brew install anomalyco/tap/opencode` or `curl -fsSL https://opencode.ai/install | bash`, then retry.",
|
||||
? "OpenCode CLI not found. Install the OpenWork-pinned OpenCode version for Windows or bundle opencode.exe with OpenWork, then restart. If it is installed, ensure `opencode.exe` is on PATH (try `opencode --version` in PowerShell)."
|
||||
: "OpenCode CLI not found. Install the OpenWork-pinned OpenCode version, then retry.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -2571,7 +2571,7 @@ export function createWorkspaceStore(options: {
|
||||
.join("\n\n");
|
||||
const suffix = serveDetails ? `\n\nServe output:\n${serveDetails}` : "";
|
||||
options.setError(
|
||||
`OpenCode CLI is installed, but \`opencode serve\` is unavailable. Update OpenCode and retry.${suffix}`
|
||||
`OpenCode CLI is installed, but \`opencode serve\` is unavailable. Update to the OpenWork-pinned OpenCode version and retry.${suffix}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -851,7 +851,7 @@ export default {
|
||||
"app.migration.desktop_required": "Migration repair requires the desktop app.",
|
||||
"app.migration.local_only": "Migration repair is only available for local workers.",
|
||||
"app.migration.workspace_required": "Pick a local workspace folder before repairing migration.",
|
||||
"app.migration.unsupported": "This OpenCode binary does not support `opencode db migrate`. Update OpenCode to >=1.2.6 or switch to bundled engine.",
|
||||
"app.migration.unsupported": "This OpenCode binary does not support `opencode db migrate`. Update to the OpenWork-pinned OpenCode version or switch to bundled engine.",
|
||||
"app.migration.failed": "OpenCode migration failed.",
|
||||
"app.migration.restart_failed": "Migration completed, but OpenWork could not restart the local engine.",
|
||||
"app.migration.success": "Migration repaired. Local startup was retried.",
|
||||
|
||||
@@ -850,7 +850,7 @@ export default {
|
||||
"app.migration.desktop_required": "マイグレーション修復にはデスクトップアプリが必要です。",
|
||||
"app.migration.local_only": "マイグレーション修復はローカルワーカーでのみ利用可能です。",
|
||||
"app.migration.workspace_required": "マイグレーション修復の前にローカルワーカーフォルダを選択してください。",
|
||||
"app.migration.unsupported": "このOpenCodeバイナリは `opencode db migrate` をサポートしていません。OpenCodeを>=1.2.6にアップデートするか、バンドルされたエンジンに切り替えてください。",
|
||||
"app.migration.unsupported": "このOpenCodeバイナリは `opencode db migrate` をサポートしていません。OpenWorkで固定しているOpenCodeバージョンへ更新するか、バンドルされたエンジンに切り替えてください。",
|
||||
"app.migration.failed": "OpenCodeのマイグレーションに失敗しました。",
|
||||
"app.migration.restart_failed": "マイグレーションは完了しましたが、OpenWorkがローカルエンジンを再起動できませんでした。",
|
||||
"app.migration.success": "マイグレーションが修復されました。ローカル起動を再試行しました。",
|
||||
|
||||
@@ -829,7 +829,7 @@ export default {
|
||||
"app.migration.desktop_required": "Sửa di chuyển dữ liệu yêu cầu ứng dụng desktop.",
|
||||
"app.migration.local_only": "Sửa di chuyển dữ liệu chỉ khả dụng cho worker nội bộ.",
|
||||
"app.migration.workspace_required": "Chọn thư mục worker nội bộ trước khi sửa di chuyển dữ liệu.",
|
||||
"app.migration.unsupported": "Bản OpenCode này không hỗ trợ `opencode db migrate`. Cập nhật lên >=1.2.6 hoặc chuyển sang engine đi kèm.",
|
||||
"app.migration.unsupported": "Bản OpenCode này không hỗ trợ `opencode db migrate`. Hãy cập nhật lên phiên bản OpenCode được OpenWork ghim cố định hoặc chuyển sang engine đi kèm.",
|
||||
"app.migration.failed": "Di chuyển dữ liệu OpenCode thất bại.",
|
||||
"app.migration.restart_failed": "Di chuyển dữ liệu hoàn tất, nhưng OpenWork không thể khởi động lại engine nội bộ.",
|
||||
"app.migration.success": "Đã sửa di chuyển dữ liệu. Đã thử khởi động lại nội bộ.",
|
||||
|
||||
@@ -789,7 +789,7 @@ export default {
|
||||
"app.migration.desktop_required": "迁移修复需要桌面应用。",
|
||||
"app.migration.local_only": "迁移修复仅适用于本地工作区。",
|
||||
"app.migration.workspace_required": "请先选择本地工作区文件夹再修复迁移。",
|
||||
"app.migration.unsupported": "当前 OpenCode 二进制不支持 `opencode db migrate`。请将 OpenCode 更新到 >=1.2.6,或切换为内置引擎。",
|
||||
"app.migration.unsupported": "当前 OpenCode 二进制不支持 `opencode db migrate`。请更新到 OpenWork 固定的 OpenCode 版本,或切换为内置引擎。",
|
||||
"app.migration.failed": "OpenCode 迁移失败。",
|
||||
"app.migration.restart_failed": "迁移已完成,但 OpenWork 无法重启本地引擎。",
|
||||
"app.migration.success": "迁移已修复,已重试本地启动。",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"name": "@openwork/desktop",
|
||||
"private": true,
|
||||
"version": "0.11.175",
|
||||
"opencodeVersion": "1.2.26",
|
||||
"opencodeRouterVersion": "0.11.175",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -33,6 +33,7 @@ const forceBuild = hasFlag("--force") || process.env.OPENWORK_SIDECAR_FORCE_BUIL
|
||||
const sidecarOverride = process.env.OPENWORK_SIDECAR_DIR?.trim() || readArg("--outdir");
|
||||
const sidecarDir = sidecarOverride ? resolve(sidecarOverride) : join(__dirname, "..", "src-tauri", "sidecars");
|
||||
const packageJsonPath = resolve(__dirname, "..", "package.json");
|
||||
const constantsPath = resolve(__dirname, "..", "..", "..", "constants.json");
|
||||
|
||||
const opencodeGithubRepo = (() => {
|
||||
const raw =
|
||||
@@ -49,15 +50,13 @@ const opencodeGithubRepo = (() => {
|
||||
return normalized;
|
||||
})();
|
||||
const opencodeVersion = (() => {
|
||||
if (process.env.OPENCODE_VERSION?.trim()) return process.env.OPENCODE_VERSION.trim();
|
||||
try {
|
||||
const raw = readFileSync(packageJsonPath, "utf8");
|
||||
const pkg = JSON.parse(raw);
|
||||
if (pkg.opencodeVersion) return String(pkg.opencodeVersion).trim();
|
||||
const raw = readFileSync(constantsPath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
return typeof parsed.opencodeVersion === "string" ? parsed.opencodeVersion.trim() || null : null;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const normalizeVersion = (value) => {
|
||||
@@ -67,29 +66,6 @@ const normalizeVersion = (value) => {
|
||||
return raw.startsWith("v") ? raw.slice(1) : raw;
|
||||
};
|
||||
|
||||
const fetchLatestOpencodeVersion = async () => {
|
||||
// Use GitHub API (no auth required). If this fails, the caller can fall back
|
||||
// to an explicitly configured version via OPENCODE_VERSION.
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10_000);
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${opencodeGithubRepo}/releases/latest`, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
const tagName = typeof data?.tag_name === "string" ? data.tag_name : "";
|
||||
return normalizeVersion(tagName);
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
};
|
||||
const opencodeAssetOverride = process.env.OPENCODE_ASSET?.trim() || null;
|
||||
const opencodeRouterVersion = (() => {
|
||||
if (process.env.OPENCODE_ROUTER_VERSION?.trim()) return process.env.OPENCODE_ROUTER_VERSION.trim();
|
||||
@@ -389,22 +365,11 @@ if (!existingOpencodeVersion && opencodeCandidatePath) {
|
||||
: null;
|
||||
}
|
||||
|
||||
// Prefer an explicitly pinned version. Otherwise, follow latest.
|
||||
const pinnedOpencodeVersion = normalizeVersion(opencodeVersion);
|
||||
let normalizedOpencodeVersion = pinnedOpencodeVersion;
|
||||
|
||||
if (!normalizedOpencodeVersion) {
|
||||
normalizedOpencodeVersion = await fetchLatestOpencodeVersion();
|
||||
}
|
||||
|
||||
// If GitHub is unreachable, fall back to whatever we already have.
|
||||
if (!normalizedOpencodeVersion && existingOpencodeVersion) {
|
||||
normalizedOpencodeVersion = normalizeVersion(existingOpencodeVersion);
|
||||
}
|
||||
const normalizedOpencodeVersion = normalizeVersion(opencodeVersion);
|
||||
|
||||
if (!normalizedOpencodeVersion) {
|
||||
console.error(
|
||||
"OpenCode version could not be resolved. Set OPENCODE_VERSION to pin a version, or ensure GitHub is reachable to use latest."
|
||||
`OpenCode version could not be resolved from ${constantsPath}.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,25 @@ fn openwork_dev_mode_enabled() -> bool {
|
||||
env_truthy("OPENWORK_DEV_MODE").unwrap_or(cfg!(debug_assertions))
|
||||
}
|
||||
|
||||
fn pinned_opencode_version() -> String {
|
||||
let constants = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../../constants.json"));
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(constants).expect("constants.json must be valid JSON");
|
||||
parsed["opencodeVersion"]
|
||||
.as_str()
|
||||
.expect("constants.json must include opencodeVersion")
|
||||
.trim()
|
||||
.trim_start_matches('v')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn pinned_opencode_install_command() -> String {
|
||||
format!(
|
||||
"curl -fsSL https://opencode.ai/install | bash -s -- --version {} --no-modify-path",
|
||||
pinned_opencode_version()
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct OutputState {
|
||||
stdout: String,
|
||||
@@ -250,7 +269,7 @@ pub fn engine_install() -> Result<ExecResult, String> {
|
||||
ok: false,
|
||||
status: -1,
|
||||
stdout: String::new(),
|
||||
stderr: "Guided install is not supported on Windows yet. Install OpenCode via Scoop/Chocolatey or https://opencode.ai/install, then restart OpenWork.".to_string(),
|
||||
stderr: "Guided install is not supported on Windows yet. Install the OpenWork-pinned OpenCode version manually, then restart OpenWork.".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,7 +282,7 @@ pub fn engine_install() -> Result<ExecResult, String> {
|
||||
|
||||
let output = std::process::Command::new("bash")
|
||||
.arg("-lc")
|
||||
.arg("curl -fsSL https://opencode.ai/install | bash")
|
||||
.arg(pinned_opencode_install_command())
|
||||
.env("OPENCODE_INSTALL_DIR", install_dir)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run installer: {e}"))?;
|
||||
@@ -363,8 +382,9 @@ pub fn engine_start(
|
||||
);
|
||||
let Some(program) = program else {
|
||||
let notes_text = notes.join("\n");
|
||||
let install_command = pinned_opencode_install_command();
|
||||
return Err(format!(
|
||||
"OpenCode CLI not found.\n\nInstall with:\n- brew install anomalyco/tap/opencode\n- curl -fsSL https://opencode.ai/install | bash\n\nNotes:\n{notes_text}"
|
||||
"OpenCode CLI not found.\n\nInstall with:\n- {install_command}\n\nNotes:\n{notes_text}"
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,24 @@ use crate::types::{ExecResult, WorkspaceOpenworkConfig};
|
||||
use crate::workspace::state::load_workspace_state;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
|
||||
fn pinned_opencode_install_command() -> String {
|
||||
let constants = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../../constants.json"
|
||||
));
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(constants).expect("constants.json must be valid JSON");
|
||||
let version = parsed["opencodeVersion"]
|
||||
.as_str()
|
||||
.expect("constants.json must include opencodeVersion")
|
||||
.trim()
|
||||
.trim_start_matches('v');
|
||||
format!(
|
||||
"curl -fsSL https://opencode.ai/install | bash -s -- --version {} --no-modify-path",
|
||||
version
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct CacheResetResult {
|
||||
pub removed: Vec<String>,
|
||||
@@ -244,8 +262,9 @@ fn resolve_opencode_program(
|
||||
|
||||
program.ok_or_else(|| {
|
||||
let notes_text = notes.join("\n");
|
||||
let install_command = pinned_opencode_install_command();
|
||||
format!(
|
||||
"OpenCode CLI not found.\n\nInstall with:\n- brew install anomalyco/tap/opencode\n- curl -fsSL https://opencode.ai/install | bash\n\nNotes:\n{notes_text}"
|
||||
"OpenCode CLI not found.\n\nInstall with:\n- {install_command}\n\nNotes:\n{notes_text}"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ Skills add new capabilities. Plugins add advanced features like scheduling or br
|
||||
OpenWork is a GUI for OpenCode. Everything that works in OpenCode works here.
|
||||
|
||||
Most reliable setup today:
|
||||
1) Install OpenCode from opencode.ai
|
||||
1) Install the OpenWork-pinned OpenCode version
|
||||
2) Configure providers there (models and API keys)
|
||||
3) Come back to OpenWork and start a session
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "openwork-orchestrator",
|
||||
"version": "0.11.175",
|
||||
"opencodeVersion": "1.2.26",
|
||||
"description": "OpenWork host orchestrator for opencode + OpenWork server + opencode-router",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -106,6 +106,21 @@ async function buildOnce(entrypoint: string, outdir: string, filename: string, t
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
const constants = JSON.parse(
|
||||
readFileSync(resolve("..", "..", "constants.json"), "utf8"),
|
||||
) as { opencodeVersion?: string };
|
||||
if (
|
||||
typeof constants.opencodeVersion === "string" &&
|
||||
constants.opencodeVersion.trim()
|
||||
) {
|
||||
define.__OPENWORK_PINNED_OPENCODE_VERSION__ = `\"${constants.opencodeVersion
|
||||
.trim()
|
||||
.replace(/^v/, "")}\"`;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const resolvedTarget = target ?? defaultTarget();
|
||||
const result = await bun.build({
|
||||
|
||||
@@ -17,6 +17,10 @@ if (!version) {
|
||||
const outroot = join(root, "dist", "npm")
|
||||
rmSync(outroot, { recursive: true, force: true })
|
||||
mkdirSync(outroot, { recursive: true })
|
||||
const constantsSrc = resolve(root, "..", "..", "constants.json")
|
||||
if (!existsSync(constantsSrc)) {
|
||||
throw new Error(`Missing constants.json at ${constantsSrc}`)
|
||||
}
|
||||
|
||||
const tag = String(process.env.NPM_TAG || "").trim()
|
||||
const dry = String(process.env.DRY_RUN || "").trim() === "1"
|
||||
@@ -101,6 +105,7 @@ if (!existsSync(postinstallSrc)) {
|
||||
throw new Error(`Missing postinstall at ${postinstallSrc}`)
|
||||
}
|
||||
copyFileSync(postinstallSrc, join(meta, basename(postinstallSrc)))
|
||||
copyFileSync(constantsSrc, join(meta, "constants.json"))
|
||||
|
||||
writeJson(join(meta, "package.json"), {
|
||||
name: "openwork-orchestrator",
|
||||
@@ -115,7 +120,7 @@ writeJson(join(meta, "package.json"), {
|
||||
postinstall: "bun ./postinstall.mjs || node ./postinstall.mjs",
|
||||
},
|
||||
optionalDependencies,
|
||||
files: ["bin", "postinstall.mjs"],
|
||||
files: ["bin", "postinstall.mjs", "constants.json"],
|
||||
})
|
||||
|
||||
published.push({ name: "openwork-orchestrator", dir: meta })
|
||||
|
||||
@@ -127,6 +127,7 @@ type OpenCodeRouterHealthSnapshot = {
|
||||
const FALLBACK_VERSION = "0.1.0";
|
||||
|
||||
declare const __OPENWORK_ORCHESTRATOR_VERSION__: string | undefined;
|
||||
declare const __OPENWORK_PINNED_OPENCODE_VERSION__: string | undefined;
|
||||
const DEFAULT_OPENWORK_PORT = 8787;
|
||||
const DEFAULT_APPROVAL_TIMEOUT = 30000;
|
||||
const DEFAULT_OPENCODE_USERNAME = "opencode";
|
||||
@@ -960,19 +961,31 @@ async function resolveCliVersion(): Promise<string> {
|
||||
return FALLBACK_VERSION;
|
||||
}
|
||||
|
||||
async function readPackageField(field: string): Promise<string | undefined> {
|
||||
async function readPinnedOpencodeVersion(): Promise<string | undefined> {
|
||||
if (
|
||||
typeof __OPENWORK_PINNED_OPENCODE_VERSION__ === "string" &&
|
||||
__OPENWORK_PINNED_OPENCODE_VERSION__.trim()
|
||||
) {
|
||||
return __OPENWORK_PINNED_OPENCODE_VERSION__.trim();
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
join(dirname(process.execPath), "..", "package.json"),
|
||||
join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
||||
join(dirname(process.execPath), "..", "constants.json"),
|
||||
join(dirname(fileURLToPath(import.meta.url)), "..", "constants.json"),
|
||||
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "constants.json"),
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (await fileExists(candidate)) {
|
||||
try {
|
||||
const raw = await readFile(candidate, "utf8");
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
const value = parsed[field];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
const parsed = JSON.parse(raw) as { opencodeVersion?: unknown };
|
||||
const value =
|
||||
typeof parsed.opencodeVersion === "string"
|
||||
? parsed.opencodeVersion.trim()
|
||||
: "";
|
||||
if (!value) continue;
|
||||
return value.startsWith("v") ? value.slice(1) : value;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1416,7 +1429,6 @@ const remoteManifestCache = new Map<
|
||||
Promise<RemoteSidecarManifest | null>
|
||||
>();
|
||||
|
||||
let latestOpencodeVersionTask: Promise<string | undefined> | null = null;
|
||||
let cachedExtraPathEntries: string[] | null = null;
|
||||
|
||||
function isDirectory(path: string): boolean {
|
||||
@@ -1713,37 +1725,6 @@ async function fetchRemoteManifest(
|
||||
return task;
|
||||
}
|
||||
|
||||
async function resolveLatestOpencodeVersion(): Promise<string | undefined> {
|
||||
if (latestOpencodeVersionTask) return latestOpencodeVersionTask;
|
||||
latestOpencodeVersionTask = (async () => {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10_000);
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/anomalyco/opencode/releases/latest",
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
if (!response.ok) return undefined;
|
||||
const data = (await response.json()) as { tag_name?: unknown };
|
||||
const tag = typeof data.tag_name === "string" ? data.tag_name.trim() : "";
|
||||
if (!tag) return undefined;
|
||||
const normalized = tag.startsWith("v") ? tag.slice(1) : tag;
|
||||
return normalized || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
})();
|
||||
return latestOpencodeVersionTask;
|
||||
}
|
||||
|
||||
function resolveAssetUrl(
|
||||
baseUrl: string,
|
||||
asset?: string,
|
||||
@@ -1792,12 +1773,16 @@ async function ensureExecutable(path: string): Promise<void> {
|
||||
async function downloadSidecarBinary(options: {
|
||||
name: SidecarName;
|
||||
sidecar: SidecarConfig;
|
||||
expectedVersion?: string;
|
||||
}): Promise<ResolvedBinary | null> {
|
||||
if (!options.sidecar.target) return null;
|
||||
const manifest = await fetchRemoteManifest(options.sidecar.manifestUrl);
|
||||
if (!manifest) return null;
|
||||
const entry = manifest.entries[options.name];
|
||||
if (!entry) return null;
|
||||
if (options.expectedVersion && entry.version !== options.expectedVersion) {
|
||||
return null;
|
||||
}
|
||||
const targetInfo = entry.targets[options.sidecar.target];
|
||||
if (!targetInfo) return null;
|
||||
|
||||
@@ -2054,8 +2039,10 @@ async function resolveExpectedVersion(
|
||||
manifest: VersionManifest | null,
|
||||
name: SidecarName,
|
||||
): Promise<string | undefined> {
|
||||
if (name !== "opencode") {
|
||||
const manifestVersion = manifest?.entries[name]?.version;
|
||||
if (manifestVersion) return manifestVersion;
|
||||
}
|
||||
|
||||
try {
|
||||
const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||
@@ -2073,16 +2060,8 @@ async function resolveExpectedVersion(
|
||||
if (localVersion) return localVersion;
|
||||
}
|
||||
if (name === "opencode") {
|
||||
const envVersion = process.env.OPENCODE_VERSION?.trim();
|
||||
if (envVersion && envVersion.toLowerCase() !== "latest") {
|
||||
return envVersion.startsWith("v") ? envVersion.slice(1) : envVersion;
|
||||
}
|
||||
const pkgVersion = await readPackageField("opencodeVersion");
|
||||
if (pkgVersion && pkgVersion.toLowerCase() !== "latest") {
|
||||
return pkgVersion.startsWith("v") ? pkgVersion.slice(1) : pkgVersion;
|
||||
}
|
||||
const latest = await resolveLatestOpencodeVersion();
|
||||
if (latest) return latest;
|
||||
const pinnedVersion = await readPinnedOpencodeVersion();
|
||||
if (pinnedVersion) return pinnedVersion;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -2450,6 +2429,7 @@ async function resolveOpencodeBin(options: {
|
||||
const downloaded = await downloadSidecarBinary({
|
||||
name: "opencode",
|
||||
sidecar: options.sidecar,
|
||||
expectedVersion,
|
||||
});
|
||||
if (downloaded) return downloaded;
|
||||
const opencodeDownloaded = await resolveOpencodeDownload(
|
||||
@@ -2460,7 +2440,7 @@ async function resolveOpencodeBin(options: {
|
||||
return { bin: opencodeDownloaded, source: "downloaded", expectedVersion };
|
||||
}
|
||||
throw new Error(
|
||||
"opencode download failed. Check sidecar manifest/network access, or set OPENCODE_VERSION to pin a version.",
|
||||
"opencode download failed. Check sidecar manifest/network access, or update constants.json.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2480,6 +2460,7 @@ async function resolveOpencodeBin(options: {
|
||||
const downloaded = await downloadSidecarBinary({
|
||||
name: "opencode",
|
||||
sidecar: options.sidecar,
|
||||
expectedVersion,
|
||||
});
|
||||
if (downloaded) return downloaded;
|
||||
|
||||
|
||||
3
constants.json
Normal file
3
constants.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"opencodeVersion": "v1.2.27"
|
||||
}
|
||||
@@ -49,7 +49,7 @@ The script prints the exact URLs and `docker compose ... down` command to use fo
|
||||
- `RENDER_WORKER_ROOT_DIR` render `rootDir` for worker services
|
||||
- `RENDER_WORKER_PLAN` Render plan for worker services
|
||||
- `RENDER_WORKER_REGION` Render region for worker services
|
||||
- `RENDER_WORKER_OPENWORK_VERSION` `openwork-orchestrator` npm version installed in workers; the worker build uses its `opencodeVersion` metadata to bundle a matching `opencode` binary into the Render deploy
|
||||
- `RENDER_WORKER_OPENWORK_VERSION` `openwork-orchestrator` npm version installed in workers; the worker build reads the pinned OpenCode version from `constants.json` shipped with that package and bundles the matching `opencode` binary into the Render deploy
|
||||
- `RENDER_WORKER_NAME_PREFIX` service name prefix
|
||||
- `RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX` optional domain suffix for worker custom URLs (e.g. `openwork.studio` -> `<worker-id>.openwork.studio`)
|
||||
- `RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS` max time to wait for vanity URL health before falling back to Render URL
|
||||
@@ -125,7 +125,7 @@ Useful optional overrides:
|
||||
- `DAYTONA_SNAPSHOT_MEMORY`
|
||||
- `DAYTONA_SNAPSHOT_DISK`
|
||||
- `OPENWORK_ORCHESTRATOR_VERSION`
|
||||
- `OPENCODE_VERSION`
|
||||
- OpenCode is pinned by `constants.json`
|
||||
|
||||
After the snapshot is pushed, set it in `.env.daytona`:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
ARG OPENWORK_ORCHESTRATOR_VERSION=0.11.151
|
||||
ARG OPENCODE_VERSION=1.2.6
|
||||
ARG OPENCODE_VERSION
|
||||
ARG OPENCODE_DOWNLOAD_URL=
|
||||
|
||||
RUN apt-get update \
|
||||
@@ -11,6 +11,7 @@ RUN apt-get update \
|
||||
RUN npm install -g "openwork-orchestrator@${OPENWORK_ORCHESTRATOR_VERSION}"
|
||||
|
||||
RUN set -eux; \
|
||||
test -n "$OPENCODE_VERSION"; \
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
amd64) asset="opencode-linux-x64-baseline.tar.gz" ;; \
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
Render worker services use this directory as `rootDir`.
|
||||
|
||||
The control plane installs `openwork-orchestrator`, runs `scripts/install-opencode.mjs` during the Render build, and then launches workers with the `openwork` command.
|
||||
The control plane installs `openwork-orchestrator`, reads the pinned OpenCode version from `constants.json` packaged with it, runs `scripts/install-opencode.mjs` during the Render build, and then launches workers with the `openwork` command.
|
||||
|
||||
That extra build step vendors the matching `opencode` release asset into `./bin/opencode` so the runtime does not depend on a first-boot GitHub download.
|
||||
|
||||
@@ -51,14 +51,18 @@ function resolveOrchestratorPackageJson() {
|
||||
}
|
||||
|
||||
function resolveOpencodeVersion() {
|
||||
const explicit = process.env.OPENCODE_VERSION?.trim();
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
const orchestratorPackageJson = resolveOrchestratorPackageJson();
|
||||
const orchestratorRoot = resolve(orchestratorPackageJson, "..");
|
||||
const versionPath = resolve(orchestratorRoot, "constants.json");
|
||||
if (!existsSync(versionPath)) {
|
||||
throw new Error(`Missing pinned constants file at ${versionPath}`);
|
||||
}
|
||||
|
||||
const orchestratorPkg = readJson(resolveOrchestratorPackageJson());
|
||||
const version = String(orchestratorPkg.opencodeVersion ?? "").trim();
|
||||
return version || null;
|
||||
const parsed = JSON.parse(readFileSync(versionPath, "utf8"));
|
||||
const version = String(parsed.opencodeVersion ?? "").trim().replace(/^v/, "");
|
||||
if (!version) {
|
||||
throw new Error(`Pinned OpenCode version is missing from ${versionPath}`);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
function resolveAssetName() {
|
||||
@@ -150,7 +154,7 @@ function findBinary(searchRoot) {
|
||||
}
|
||||
|
||||
const version = resolveOpencodeVersion();
|
||||
const versionLabel = version ?? "latest";
|
||||
const versionLabel = version;
|
||||
|
||||
if (
|
||||
existsSync(outputPath) &&
|
||||
@@ -165,7 +169,10 @@ const assetName = resolveAssetName();
|
||||
const downloadUrl = process.env.OPENWORK_OPENCODE_DOWNLOAD_URL?.trim()
|
||||
|| (version
|
||||
? `https://github.com/anomalyco/opencode/releases/download/v${version}/${assetName}`
|
||||
: `https://github.com/anomalyco/opencode/releases/latest/download/${assetName}`);
|
||||
: null);
|
||||
if (!downloadUrl) {
|
||||
throw new Error("Pinned OpenCode version is required to bundle the worker runtime");
|
||||
}
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "den-worker-opencode-"));
|
||||
const archivePath = join(tempDir, assetName);
|
||||
const extractDir = join(tempDir, "extract");
|
||||
|
||||
@@ -35,7 +35,7 @@ SNAPSHOT_DISK="${DAYTONA_SNAPSHOT_DISK:-8}"
|
||||
LOCAL_IMAGE_TAG="${DAYTONA_LOCAL_IMAGE_TAG:-openwork-daytona-snapshot:${SNAPSHOT_NAME//[^a-zA-Z0-9_.-]/-}}"
|
||||
|
||||
OPENWORK_ORCHESTRATOR_VERSION="${OPENWORK_ORCHESTRATOR_VERSION:-$(node -e 'const fs=require("fs"); const pkg=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(String(pkg.version));' "$ROOT_DIR/apps/orchestrator/package.json")}"
|
||||
OPENCODE_VERSION="${OPENCODE_VERSION:-$(node -e 'const fs=require("fs"); const pkg=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(String(pkg.opencodeVersion));' "$ROOT_DIR/apps/orchestrator/package.json")}"
|
||||
OPENCODE_VERSION="$(node -e 'const fs=require("fs"); const parsed=JSON.parse(fs.readFileSync(process.argv[1], "utf8")); process.stdout.write(String(parsed.opencodeVersion || "").trim().replace(/^v/, ""));' "$ROOT_DIR/constants.json")"
|
||||
|
||||
echo "Building local image $LOCAL_IMAGE_TAG" >&2
|
||||
echo "- openwork-orchestrator@$OPENWORK_ORCHESTRATOR_VERSION" >&2
|
||||
|
||||
@@ -21,6 +21,11 @@ const desktopPkg = readJson(resolve(root, "apps", "desktop", "package.json"));
|
||||
const orchestratorPkg = readJson(
|
||||
resolve(root, "apps", "orchestrator", "package.json"),
|
||||
);
|
||||
const pinnedOpencodeVersion = String(
|
||||
readJson(resolve(root, "constants.json")).opencodeVersion ?? "",
|
||||
)
|
||||
.trim()
|
||||
.replace(/^v/, "");
|
||||
const serverPkg = readJson(resolve(root, "apps", "server", "package.json"));
|
||||
const opencodeRouterPkg = readJson(
|
||||
resolve(root, "apps", "opencode-router", "package.json"),
|
||||
@@ -40,10 +45,7 @@ const versions = {
|
||||
server: serverPkg.version ?? null,
|
||||
orchestrator: orchestratorPkg.version ?? null,
|
||||
opencodeRouter: opencodeRouterPkg.version ?? null,
|
||||
opencode: {
|
||||
desktop: desktopPkg.opencodeVersion ?? null,
|
||||
orchestrator: orchestratorPkg.opencodeVersion ?? null,
|
||||
},
|
||||
opencode: pinnedOpencodeVersion || null,
|
||||
opencodeRouterVersionPinned: desktopPkg.opencodeRouterVersion ?? null,
|
||||
orchestratorOpenworkServerRange:
|
||||
orchestratorPkg.dependencies?.["openwork-server"] ?? null,
|
||||
@@ -101,17 +103,15 @@ addCheck(
|
||||
versions.opencodeRouter === versions.opencodeRouterVersionPinned,
|
||||
`${versions.opencodeRouterVersionPinned ?? "?"} vs ${versions.opencodeRouter ?? "?"}`,
|
||||
);
|
||||
if (versions.opencode.desktop || versions.opencode.orchestrator) {
|
||||
if (versions.opencode) {
|
||||
addCheck(
|
||||
"OpenCode version matches (desktop/orchestrator)",
|
||||
versions.opencode.desktop &&
|
||||
versions.opencode.orchestrator &&
|
||||
versions.opencode.desktop === versions.opencode.orchestrator,
|
||||
`${versions.opencode.desktop ?? "?"} vs ${versions.opencode.orchestrator ?? "?"}`,
|
||||
"OpenCode version pin exists",
|
||||
Boolean(versions.opencode),
|
||||
String(versions.opencode),
|
||||
);
|
||||
} else {
|
||||
addWarning(
|
||||
"OpenCode version is not pinned (apps/desktop + apps/orchestrator). Sidecar bundling will default to the latest OpenCode release at build time.",
|
||||
"OpenCode version is not pinned in constants.json.",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user