diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml deleted file mode 100644 index a7106667b116..000000000000 --- a/.github/workflows/beta.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: beta - -on: - workflow_dispatch: - schedule: - - cron: "0 * * * *" - -jobs: - sync: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Setup Git Committer - id: setup-git-committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Install OpenCode - run: bun i -g opencode-ai - - - name: Sync beta branch - env: - GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - run: bun script/beta.ts diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml deleted file mode 100644 index 04b6ae7ac80f..000000000000 --- a/.github/workflows/close-issues.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: close-issues - -on: - schedule: - - cron: "0 2 * * *" # Daily at 2:00 AM - workflow_dispatch: - -jobs: - close: - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - steps: - - uses: actions/checkout@v4 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Close stale issues - env: - GITHUB_TOKEN: ${{ github.token }} - run: bun script/github/close-issues.ts diff --git a/.github/workflows/close-stale-prs.yml b/.github/workflows/close-stale-prs.yml deleted file mode 100644 index e0e571b46911..000000000000 --- a/.github/workflows/close-stale-prs.yml +++ /dev/null @@ -1,235 +0,0 @@ -name: close-stale-prs - -on: - workflow_dispatch: - inputs: - dryRun: - description: "Log actions without closing PRs" - type: boolean - default: false - schedule: - - cron: "0 6 * * *" - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - close-stale-prs: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Close inactive PRs - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const DAYS_INACTIVE = 60 - const MAX_RETRIES = 3 - - // Adaptive delay: fast for small batches, slower for large to respect - // GitHub's 80 content-generating requests/minute limit - const SMALL_BATCH_THRESHOLD = 10 - const SMALL_BATCH_DELAY_MS = 1000 // 1s for daily operations (≤10 PRs) - const LARGE_BATCH_DELAY_MS = 2000 // 2s for backlog (>10 PRs) = ~30 ops/min, well under 80 limit - - const startTime = Date.now() - const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000) - const { owner, repo } = context.repo - const dryRun = context.payload.inputs?.dryRun === "true" - - core.info(`Dry run mode: ${dryRun}`) - core.info(`Cutoff date: ${cutoff.toISOString()}`) - - function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)) - } - - async function withRetry(fn, description = 'API call') { - let lastError - for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { - try { - const result = await fn() - return result - } catch (error) { - lastError = error - const isRateLimited = error.status === 403 && - (error.message?.includes('rate limit') || error.message?.includes('secondary')) - - if (!isRateLimited) { - throw error - } - - // Parse retry-after header, default to 60 seconds - const retryAfter = error.response?.headers?.['retry-after'] - ? parseInt(error.response.headers['retry-after']) - : 60 - - // Exponential backoff: retryAfter * 2^attempt - const backoffMs = retryAfter * 1000 * Math.pow(2, attempt) - - core.warning(`${description}: Rate limited (attempt ${attempt + 1}/${MAX_RETRIES}). Waiting ${backoffMs / 1000}s before retry...`) - - await sleep(backoffMs) - } - } - core.error(`${description}: Max retries (${MAX_RETRIES}) exceeded`) - throw lastError - } - - const query = ` - query($owner: String!, $repo: String!, $cursor: String) { - repository(owner: $owner, name: $repo) { - pullRequests(first: 100, states: OPEN, after: $cursor) { - pageInfo { - hasNextPage - endCursor - } - nodes { - number - title - author { - login - } - createdAt - commits(last: 1) { - nodes { - commit { - committedDate - } - } - } - comments(last: 1) { - nodes { - createdAt - } - } - reviews(last: 1) { - nodes { - createdAt - } - } - } - } - } - } - ` - - const allPrs = [] - let cursor = null - let hasNextPage = true - let pageCount = 0 - - while (hasNextPage) { - pageCount++ - core.info(`Fetching page ${pageCount} of open PRs...`) - - const result = await withRetry( - () => github.graphql(query, { owner, repo, cursor }), - `GraphQL page ${pageCount}` - ) - - allPrs.push(...result.repository.pullRequests.nodes) - hasNextPage = result.repository.pullRequests.pageInfo.hasNextPage - cursor = result.repository.pullRequests.pageInfo.endCursor - - core.info(`Page ${pageCount}: fetched ${result.repository.pullRequests.nodes.length} PRs (total: ${allPrs.length})`) - - // Delay between pagination requests (use small batch delay for reads) - if (hasNextPage) { - await sleep(SMALL_BATCH_DELAY_MS) - } - } - - core.info(`Found ${allPrs.length} open pull requests`) - - const stalePrs = allPrs.filter((pr) => { - const dates = [ - new Date(pr.createdAt), - pr.commits.nodes[0] ? new Date(pr.commits.nodes[0].commit.committedDate) : null, - pr.comments.nodes[0] ? new Date(pr.comments.nodes[0].createdAt) : null, - pr.reviews.nodes[0] ? new Date(pr.reviews.nodes[0].createdAt) : null, - ].filter((d) => d !== null) - - const lastActivity = dates.sort((a, b) => b.getTime() - a.getTime())[0] - - if (!lastActivity || lastActivity > cutoff) { - core.info(`PR #${pr.number} is fresh (last activity: ${lastActivity?.toISOString() || "unknown"})`) - return false - } - - core.info(`PR #${pr.number} is STALE (last activity: ${lastActivity.toISOString()})`) - return true - }) - - if (!stalePrs.length) { - core.info("No stale pull requests found.") - return - } - - core.info(`Found ${stalePrs.length} stale pull requests`) - - // ============================================ - // Close stale PRs - // ============================================ - const requestDelayMs = stalePrs.length > SMALL_BATCH_THRESHOLD - ? LARGE_BATCH_DELAY_MS - : SMALL_BATCH_DELAY_MS - - core.info(`Using ${requestDelayMs}ms delay between operations (${stalePrs.length > SMALL_BATCH_THRESHOLD ? 'large' : 'small'} batch mode)`) - - let closedCount = 0 - let skippedCount = 0 - - for (const pr of stalePrs) { - const issue_number = pr.number - const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.` - - if (dryRun) { - core.info(`[dry-run] Would close PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`) - continue - } - - try { - // Add comment - await withRetry( - () => github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: closeComment, - }), - `Comment on PR #${issue_number}` - ) - - // Close PR - await withRetry( - () => github.rest.pulls.update({ - owner, - repo, - pull_number: issue_number, - state: "closed", - }), - `Close PR #${issue_number}` - ) - - closedCount++ - core.info(`Closed PR #${issue_number} from ${pr.author?.login || 'unknown'}: ${pr.title}`) - - // Delay before processing next PR - await sleep(requestDelayMs) - } catch (error) { - skippedCount++ - core.error(`Failed to close PR #${issue_number}: ${error.message}`) - } - } - - const elapsed = Math.round((Date.now() - startTime) / 1000) - core.info(`\n========== Summary ==========`) - core.info(`Total open PRs found: ${allPrs.length}`) - core.info(`Stale PRs identified: ${stalePrs.length}`) - core.info(`PRs closed: ${closedCount}`) - core.info(`PRs skipped (errors): ${skippedCount}`) - core.info(`Elapsed time: ${elapsed}s`) - core.info(`=============================`) diff --git a/.github/workflows/compliance-close.yml b/.github/workflows/compliance-close.yml deleted file mode 100644 index c3bcf9f686f4..000000000000 --- a/.github/workflows/compliance-close.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: compliance-close - -on: - schedule: - # Run every 30 minutes to check for expired compliance windows - - cron: "*/30 * * * *" - workflow_dispatch: - -permissions: - contents: read - issues: write - pull-requests: write - -jobs: - close-non-compliant: - runs-on: ubuntu-latest - steps: - - name: Close non-compliant issues and PRs after 2 hours - uses: actions/github-script@v7 - with: - script: | - const { data: items } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: 'needs:compliance', - state: 'open', - per_page: 100, - }); - - if (items.length === 0) { - core.info('No open issues/PRs with needs:compliance label'); - return; - } - - const now = Date.now(); - const twoHours = 2 * 60 * 60 * 1000; - - for (const item of items) { - const isPR = !!item.pull_request; - const kind = isPR ? 'PR' : 'issue'; - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - }); - - const complianceComment = comments.find(c => c.body.includes('')); - if (!complianceComment) continue; - - const commentAge = now - new Date(complianceComment.created_at).getTime(); - if (commentAge < twoHours) { - core.info(`${kind} #${item.number} still within 2-hour window (${Math.round(commentAge / 60000)}m elapsed)`); - continue; - } - - const closeMessage = isPR - ? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new pull request that follows our guidelines.' - : 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) within the 2-hour window.\n\nFeel free to open a new issue that follows our issue templates.'; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - body: closeMessage, - }); - - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - name: 'needs:compliance', - }); - } catch (e) {} - - if (isPR) { - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: item.number, - state: 'closed', - }); - } else { - await github.rest.issues.update({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: item.number, - state: 'closed', - state_reason: 'not_planned', - }); - } - - core.info(`Closed non-compliant ${kind} #${item.number} after 2-hour window`); - } diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml deleted file mode 100644 index c7df066d41c6..000000000000 --- a/.github/workflows/containers.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: containers - -on: - push: - branches: - - dev - paths: - - packages/containers/** - - .github/workflows/containers.yml - - package.json - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - build: - runs-on: blacksmith-4vcpu-ubuntu-2404 - env: - REGISTRY: ghcr.io/${{ github.repository_owner }} - TAG: "24.04" - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-bun - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push containers - run: bun ./packages/containers/script/build.ts --push - env: - REGISTRY: ${{ env.REGISTRY }} - TAG: ${{ env.TAG }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index abd8bafdd675..000000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: deploy - -on: - push: - branches: - - dev - - production - workflow_dispatch: - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: ./.github/actions/setup-bun - - - uses: actions/setup-node@v4 - with: - node-version: "24" - - # Workaround for Pulumi version conflict: - # GitHub runners have Pulumi 3.212.0+ pre-installed, which removed the -root flag - # from pulumi-language-nodejs (see https://github.com/pulumi/pulumi/pull/21065). - # SST 3.17.x uses Pulumi SDK 3.210.0 which still passes -root, causing a conflict. - # Removing the system language plugin forces SST to use its bundled compatible version. - # TODO: Remove when sst supports Pulumi >3.210.0 - - name: Fix Pulumi version conflict - run: sudo rm -f /usr/local/bin/pulumi-language-nodejs - - - run: bun sst deploy --stage=${{ github.ref_name }} - env: - CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }} - PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }} - STRIPE_SECRET_KEY: ${{ github.ref_name == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }} - HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} - SENTRY_RELEASE: web@${{ github.sha }} - VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }} - VITE_SENTRY_RELEASE: web@${{ github.sha }} diff --git a/.github/workflows/desktop-arm64-portable.yml b/.github/workflows/desktop-arm64-portable.yml new file mode 100644 index 000000000000..35be7ada6911 --- /dev/null +++ b/.github/workflows/desktop-arm64-portable.yml @@ -0,0 +1,34 @@ +name: desktop-arm64-portable + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + build: + runs-on: windows-2025 + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Install deps + run: bun install --ignore-scripts + + - name: Build portable + working-directory: packages/desktop + run: | + $env:RUST_TARGET = "aarch64-pc-windows-msvc" + bun ./scripts/prepare.ts + bun install --ignore-scripts + bun run build + npx electron-builder --win --arm64 --publish never --config electron-builder.config.ts + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: opencode-portable + path: packages/desktop/dist/*portable*.exe \ No newline at end of file diff --git a/.github/workflows/docs-locale-sync.yml b/.github/workflows/docs-locale-sync.yml deleted file mode 100644 index 9689eee6d212..000000000000 --- a/.github/workflows/docs-locale-sync.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: docs-locale-sync - -on: - push: - branches: - - dev - paths: - - packages/web/src/content/docs/*.mdx - -jobs: - sync-locales: - if: false - #if: github.actor != 'opencode-agent[bot]' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Compute changed English docs - id: changes - run: | - FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}" -- ':(glob)packages/web/src/content/docs/*.mdx' || true) - if [ -z "$FILES" ]; then - echo "has_changes=false" >> "$GITHUB_OUTPUT" - echo "No English docs changed in push range" - exit 0 - fi - echo "has_changes=true" >> "$GITHUB_OUTPUT" - { - echo "files<> "$GITHUB_OUTPUT" - - - name: Install OpenCode - if: steps.changes.outputs.has_changes == 'true' - run: curl -fsSL https://opencode.ai/install | bash - - - name: Sync locale docs with OpenCode - if: steps.changes.outputs.has_changes == 'true' - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - OPENCODE_CONFIG_CONTENT: | - { - "permission": { - "*": "deny", - "read": "allow", - "edit": "allow", - "glob": "allow", - "task": "allow" - } - } - run: | - opencode run --agent docs --model opencode/gpt-5.3-codex <<'EOF' - Update localized docs to match the latest English docs changes. - - Changed English doc files: - - ${{ steps.changes.outputs.files }} - - - Requirements: - 1. Update all relevant locale docs under packages/web/src/content/docs// so they reflect these English page changes. - 2. You MUST use the Task tool for translation work and launch subagents with subagent_type `translator` (defined in .opencode/agent/translator.md). - 3. Do not translate directly in the primary agent. Use translator subagent output as the source for locale text updates. - 4. Run translator subagent Task calls in parallel whenever file/locale translation work is independent. - 5. Use only the minimum tools needed for this task (read/glob, file edits, and translator Task). Do not use shell, web, search, or GitHub tools for translation work. - 6. Preserve frontmatter keys, internal links, code blocks, and existing locale-specific metadata unless the English change requires an update. - 7. Keep locale docs structure aligned with their corresponding English pages. - 8. Do not modify English source docs in packages/web/src/content/docs/*.mdx. - 9. If no locale updates are needed, make no changes. - EOF - - - name: Commit and push locale docs updates - if: steps.changes.outputs.has_changes == 'true' - run: | - if [ -z "$(git status --porcelain)" ]; then - echo "No locale docs changes to commit" - exit 0 - fi - git add -A - git commit -m "docs(i18n): sync locale docs from english changes" - git pull --rebase --autostash origin "$GITHUB_REF_NAME" - git push origin HEAD:"$GITHUB_REF_NAME" diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml deleted file mode 100644 index 900ad2b0c586..000000000000 --- a/.github/workflows/docs-update.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: docs-update - -on: - schedule: - - cron: "0 */12 * * *" - workflow_dispatch: - -env: - LOOKBACK_HOURS: 4 - -jobs: - update-docs: - if: github.repository == 'sst/opencode' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - id-token: write - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch full history to access commits - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Get recent commits - id: commits - run: | - COMMITS=$(git log --since="${{ env.LOOKBACK_HOURS }} hours ago" --pretty=format:"- %h %s" 2>/dev/null || echo "") - if [ -z "$COMMITS" ]; then - echo "No commits in the last ${{ env.LOOKBACK_HOURS }} hours" - echo "has_commits=false" >> $GITHUB_OUTPUT - else - echo "has_commits=true" >> $GITHUB_OUTPUT - { - echo "list<> $GITHUB_OUTPUT - fi - - - name: Run opencode - if: steps.commits.outputs.has_commits == 'true' - uses: sst/opencode/github@latest - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - with: - model: opencode/gpt-5.2 - agent: docs - prompt: | - Review the following commits from the last ${{ env.LOOKBACK_HOURS }} hours and identify any new features that may need documentation. - - - ${{ steps.commits.outputs.list }} - - - Steps: - 1. For each commit that looks like a new feature or significant change: - - Read the changed files to understand what was added - - Check if the feature is already documented in packages/web/src/content/docs/* - 2. If you find undocumented features: - - Update the relevant documentation files in packages/web/src/content/docs/* - - Follow the existing documentation style and structure - - Make sure to document the feature clearly with examples where appropriate - 3. If all new features are already documented, report that no updates are needed - 4. If you are creating a new documentation file be sure to update packages/web/astro.config.mjs too. - - Focus on user-facing features and API changes. Skip internal refactors, bug fixes, and test updates unless they affect user-facing behavior. - Don't feel the need to document every little thing. It is perfectly okay to make 0 changes at all. - Try to keep documentation only for large features or changes that already have a good spot to be documented. diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml deleted file mode 100644 index 6c1943fe7b8a..000000000000 --- a/.github/workflows/duplicate-issues.yml +++ /dev/null @@ -1,177 +0,0 @@ -name: duplicate-issues - -on: - issues: - types: [opened, edited] - -jobs: - check-duplicates: - if: github.event.action == 'opened' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Check duplicates and compliance - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENCODE_PERMISSION: | - { - "bash": { - "*": "deny", - "gh issue*": "allow" - }, - "webfetch": "deny" - } - run: | - opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: - - Issue number: ${{ github.event.issue.number }} - - Lookup this issue with gh issue view ${{ github.event.issue.number }}. - - You have TWO tasks. Perform both, then post a SINGLE comment (if needed). - - --- - - TASK 1: CONTRIBUTING GUIDELINES COMPLIANCE CHECK - - Check whether the issue follows our contributing guidelines and issue templates. - - This project has three issue templates that every issue MUST use one of: - - 1. Bug Report - requires a Description field with real content - 2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]: - 3. Question - requires the Question field with real content - - Additionally check: - - No AI-generated walls of text (long, AI-generated descriptions are not acceptable) - - The issue has real content, not just template placeholder text left unchanged - - Bug reports should include some context about how to reproduce - - Feature requests should explain the problem or need - - We want to push for having the user provide system description & information - - Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content. - - --- - - TASK 2: DUPLICATE CHECK - - Search through existing issues (excluding #${{ github.event.issue.number }}) to find potential duplicates. - Consider: - 1. Similar titles or descriptions - 2. Same error messages or symptoms - 3. Related functionality or components - 4. Similar feature requests - - Additionally, if the issue mentions keybinds, keyboard shortcuts, or key bindings, note the pinned keybinds issue #4997. - - --- - - POSTING YOUR COMMENT: - - Based on your findings, post a SINGLE comment on issue #${{ github.event.issue.number }}. Build the comment as follows: - - If the issue is NOT compliant, start the comment with: - - Then explain what needs to be fixed and that they have 2 hours to edit the issue before it is automatically closed. Also add the label needs:compliance to the issue using: gh issue edit ${{ github.event.issue.number }} --add-label needs:compliance - - If duplicates were found, include a section about potential duplicates with links. - - If the issue mentions keybinds/keyboard shortcuts, include a note about #4997. - - If the issue IS compliant AND no duplicates were found AND no keybind reference, do NOT comment at all. - - Use this format for the comment: - - [If not compliant:] - - This issue doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md). - - **What needs to be fixed:** - - [specific reasons] - - Please edit this issue to address the above within **2 hours**, or it will be automatically closed. - - [If duplicates found, add:] - --- - This issue might be a duplicate of existing issues. Please check: - - #[issue_number]: [brief description of similarity] - - [If keybind-related, add:] - For keybind-related issues, please also check our pinned keybinds documentation: #4997 - - [End with if not compliant:] - If you believe this was flagged incorrectly, please let a maintainer know. - - Remember: post at most ONE comment combining all findings. If everything is fine, post nothing." - - recheck-compliance: - if: github.event.action == 'edited' && contains(github.event.issue.labels.*.name, 'needs:compliance') - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Recheck compliance - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENCODE_PERMISSION: | - { - "bash": { - "*": "deny", - "gh issue*": "allow" - }, - "webfetch": "deny" - } - run: | - opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. - - Lookup this issue with gh issue view ${{ github.event.issue.number }}. - - Re-check whether the issue now follows our contributing guidelines and issue templates. - - This project has three issue templates that every issue MUST use one of: - - 1. Bug Report - requires a Description field with real content - 2. Feature Request - requires a verification checkbox and description, title should start with [FEATURE]: - 3. Question - requires the Question field with real content - - Additionally check: - - No AI-generated walls of text (long, AI-generated descriptions are not acceptable) - - The issue has real content, not just template placeholder text left unchanged - - Bug reports should include some context about how to reproduce - - Feature requests should explain the problem or need - - We want to push for having the user provide system description & information - - Do NOT be nitpicky about optional fields. Only flag real problems like: no template used, required fields empty or placeholder text only, obviously AI-generated walls of text, or completely empty/nonsensical content. - - If the issue is NOW compliant: - 1. Remove the needs:compliance label: gh issue edit ${{ github.event.issue.number }} --remove-label needs:compliance - 2. Find and delete the previous compliance comment (the one containing ) using: gh api repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments --jq '.[] | select(.body | contains(\"\")) | .id' then delete it with: gh api -X DELETE repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments/{id} - 3. Post a short comment thanking them for updating the issue. - - If the issue is STILL not compliant: - Post a comment explaining what still needs to be fixed. Keep the needs:compliance label." diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml deleted file mode 100644 index 706ab2989e15..000000000000 --- a/.github/workflows/generate.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: generate - -on: - push: - branches: - - dev - -jobs: - generate: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Generate - run: ./script/generate.ts - - - name: Commit and push - run: | - if [ -z "$(git status --porcelain)" ]; then - echo "No changes to commit" - exit 0 - fi - git add -A - git commit -m "chore: generate" --allow-empty - git push origin HEAD:${{ github.ref_name }} --no-verify - # if ! git push origin HEAD:${{ github.event.pull_request.head.ref || github.ref_name }} --no-verify; then - # echo "" - # echo "============================================" - # echo "Failed to push generated code." - # echo "Please run locally and push:" - # echo "" - # echo " ./script/generate.ts" - # echo " git add -A && git commit -m \"chore: generate\" && git push" - # echo "" - # echo "============================================" - # exit 1 - # fi diff --git a/.github/workflows/nix-eval.yml b/.github/workflows/nix-eval.yml deleted file mode 100644 index c76b2c972973..000000000000 --- a/.github/workflows/nix-eval.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: nix-eval - -on: - push: - branches: [dev] - pull_request: - branches: [dev] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - nix-eval: - runs-on: blacksmith-4vcpu-ubuntu-2404 - timeout-minutes: 15 - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Nix - uses: nixbuild/nix-quick-install-action@v34 - - - name: Evaluate flake outputs (all systems) - run: | - set -euo pipefail - nix --version - - echo "=== Flake metadata ===" - nix flake metadata - - echo "" - echo "=== Flake structure ===" - nix flake show --all-systems - - SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" - PACKAGES="opencode" - # TODO: move 'desktop' to PACKAGES when #11755 is fixed - OPTIONAL_PACKAGES="desktop" - - echo "" - echo "=== Evaluating packages for all systems ===" - for system in $SYSTEMS; do - echo "" - echo "--- $system ---" - for pkg in $PACKAGES; do - printf " %s: " "$pkg" - if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then - echo "✓" - else - echo "✗" - echo "::error::Evaluation failed for packages.$system.$pkg" - echo "$output" - exit 1 - fi - done - done - - echo "" - echo "=== Evaluating optional packages ===" - for system in $SYSTEMS; do - echo "" - echo "--- $system ---" - for pkg in $OPTIONAL_PACKAGES; do - printf " %s: " "$pkg" - if output=$(nix eval ".#packages.$system.$pkg.drvPath" --raw 2>&1); then - echo "✓" - else - echo "✗" - echo "::warning::Evaluation failed for packages.$system.$pkg" - echo "$output" - fi - done - done - - echo "" - echo "=== Evaluating devShells for all systems ===" - for system in $SYSTEMS; do - printf "%s: " "$system" - if output=$(nix eval ".#devShells.$system.default.drvPath" --raw 2>&1); then - echo "✓" - else - echo "✗" - echo "::error::Evaluation failed for devShells.$system.default" - echo "$output" - exit 1 - fi - done - - echo "" - echo "=== All evaluations passed ===" diff --git a/.github/workflows/nix-hashes.yml b/.github/workflows/nix-hashes.yml deleted file mode 100644 index 6b5b3929adcb..000000000000 --- a/.github/workflows/nix-hashes.yml +++ /dev/null @@ -1,152 +0,0 @@ -name: nix-hashes - -permissions: - contents: write - -on: - workflow_dispatch: - push: - branches: [dev, beta] - paths: - - "bun.lock" - - "package.json" - - "packages/*/package.json" - - "flake.lock" - - "nix/node_modules.nix" - - "nix/scripts/**" - - "patches/**" - - ".github/workflows/nix-hashes.yml" - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - # Native runners required: bun install cross-compilation flags (--os/--cpu) - # do not produce byte-identical node_modules as native installs. - compute-hash: - strategy: - fail-fast: false - matrix: - include: - - system: x86_64-linux - runner: blacksmith-4vcpu-ubuntu-2404 - - system: aarch64-linux - runner: blacksmith-4vcpu-ubuntu-2404-arm - - system: x86_64-darwin - runner: macos-15-intel - - system: aarch64-darwin - runner: macos-latest - runs-on: ${{ matrix.runner }} - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Setup Nix - uses: nixbuild/nix-quick-install-action@v34 - - - name: Compute node_modules hash - id: hash - env: - SYSTEM: ${{ matrix.system }} - run: | - set -euo pipefail - - BUILD_LOG=$(mktemp) - trap 'rm -f "$BUILD_LOG"' EXIT - - # Build with fakeHash to trigger hash mismatch and reveal correct hash - nix build ".#packages.${SYSTEM}.node_modules_updater" --no-link 2>&1 | tee "$BUILD_LOG" || true - - # Extract hash from build log with portability - HASH="$(nix run --inputs-from . nixpkgs#gnugrep -- -oP 'got:\s*\Ksha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | tail -n1 || true)" - - if [ -z "$HASH" ]; then - echo "::error::Failed to compute hash for ${SYSTEM}" - cat "$BUILD_LOG" - exit 1 - fi - - echo "$HASH" > hash.txt - echo "Computed hash for ${SYSTEM}: $HASH" - - - name: Upload hash - uses: actions/upload-artifact@v4 - with: - name: hash-${{ matrix.system }} - path: hash.txt - retention-days: 1 - - update-hashes: - needs: compute-hash - if: github.event_name != 'pull_request' - runs-on: blacksmith-4vcpu-ubuntu-2404 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - persist-credentials: false - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Setup git committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Pull latest changes - run: | - git pull --rebase --autostash origin "$GITHUB_REF_NAME" - - - name: Download hash artifacts - uses: actions/download-artifact@v4 - with: - path: hashes - pattern: hash-* - - - name: Update hashes.json - run: | - set -euo pipefail - - HASH_FILE="nix/hashes.json" - - [ -f "$HASH_FILE" ] || echo '{"nodeModules":{}}' > "$HASH_FILE" - - for SYSTEM in x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin; do - FILE="hashes/hash-${SYSTEM}/hash.txt" - if [ -f "$FILE" ]; then - HASH="$(tr -d '[:space:]' < "$FILE")" - echo "${SYSTEM}: ${HASH}" - jq --arg sys "$SYSTEM" --arg h "$HASH" '.nodeModules[$sys] = $h' "$HASH_FILE" > tmp.json - mv tmp.json "$HASH_FILE" - else - echo "::warning::Missing hash for ${SYSTEM}" - fi - done - - cat "$HASH_FILE" - - - name: Commit changes - run: | - set -euo pipefail - - HASH_FILE="nix/hashes.json" - - if [ -z "$(git status --short -- "$HASH_FILE")" ]; then - echo "No changes to commit" - echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY" - echo "Status: no changes" >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - git add "$HASH_FILE" - git commit -m "chore: update nix node_modules hashes" - - git pull --rebase --autostash origin "$GITHUB_REF_NAME" - git push origin HEAD:"$GITHUB_REF_NAME" - - echo "### Nix hashes" >> "$GITHUB_STEP_SUMMARY" - echo "Status: committed $(git rev-parse --short HEAD)" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/notify-discord.yml b/.github/workflows/notify-discord.yml deleted file mode 100644 index b1d8053603a9..000000000000 --- a/.github/workflows/notify-discord.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: notify-discord - -on: - release: - types: [released] # fires when a draft release is published - -jobs: - notify: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Send nicely-formatted embed to Discord - uses: SethCohen/github-releases-to-discord@v1 - with: - webhook_url: ${{ secrets.DISCORD_WEBHOOK }} diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml deleted file mode 100644 index 76e75fcaefb9..000000000000 --- a/.github/workflows/opencode.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: opencode - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -jobs: - opencode: - if: | - contains(github.event.comment.body, ' /oc') || - startsWith(github.event.comment.body, '/oc') || - contains(github.event.comment.body, ' /opencode') || - startsWith(github.event.comment.body, '/opencode') - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - id-token: write - contents: read - pull-requests: read - issues: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - uses: ./.github/actions/setup-bun - - - name: Run opencode - uses: anomalyco/opencode/github@latest - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - OPENCODE_PERMISSION: '{"bash": "deny"}' - with: - model: opencode/claude-opus-4-5 diff --git a/.github/workflows/pr-management.yml b/.github/workflows/pr-management.yml deleted file mode 100644 index 35bd7ae36f2d..000000000000 --- a/.github/workflows/pr-management.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: pr-management - -on: - pull_request_target: - types: [opened] - -jobs: - check-duplicates: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Check team membership - id: team-check - run: | - LOGIN="${{ github.event.pull_request.user.login }}" - if [ "$LOGIN" = "opencode-agent[bot]" ] || grep -qxF "$LOGIN" .github/TEAM_MEMBERS; then - echo "is_team=true" >> "$GITHUB_OUTPUT" - echo "Skipping: $LOGIN is a team member or bot" - else - echo "is_team=false" >> "$GITHUB_OUTPUT" - fi - - - name: Setup Bun - if: steps.team-check.outputs.is_team != 'true' - uses: ./.github/actions/setup-bun - - - name: Install dependencies - if: steps.team-check.outputs.is_team != 'true' - run: bun install - - - name: Install opencode - if: steps.team-check.outputs.is_team != 'true' - run: curl -fsSL https://opencode.ai/install | bash - - - name: Build prompt - if: steps.team-check.outputs.is_team != 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - { - echo "Check for duplicate PRs related to this new PR:" - echo "" - echo "CURRENT_PR_NUMBER: $PR_NUMBER" - echo "" - echo "Title: $(gh pr view "$PR_NUMBER" --json title --jq .title)" - echo "" - echo "Description:" - gh pr view "$PR_NUMBER" --json body --jq .body - } > pr_info.txt - - - name: Check for duplicate PRs - if: steps.team-check.outputs.is_team != 'true' - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: | - COMMENT=$(bun script/duplicate-pr.ts -f pr_info.txt "Check the attached file for PR details and search for duplicates") - - if [ "$COMMENT" != "No duplicate PRs found" ]; then - gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_ - - $COMMENT" - fi - - add-contributor-label: - runs-on: ubuntu-latest - permissions: - pull-requests: write - issues: write - steps: - - name: Add Contributor Label - uses: actions/github-script@v8 - with: - script: | - const isPR = !!context.payload.pull_request; - const issueNumber = isPR ? context.payload.pull_request.number : context.payload.issue.number; - const authorAssociation = isPR ? context.payload.pull_request.author_association : context.payload.issue.author_association; - - if (authorAssociation === 'CONTRIBUTOR') { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issueNumber, - labels: ['contributor'] - }); - } diff --git a/.github/workflows/pr-standards.yml b/.github/workflows/pr-standards.yml deleted file mode 100644 index 1edbd5d061dc..000000000000 --- a/.github/workflows/pr-standards.yml +++ /dev/null @@ -1,351 +0,0 @@ -name: pr-standards - -on: - pull_request_target: - types: [opened, edited, synchronize] - -jobs: - check-standards: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Check PR standards - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request; - const login = pr.user.login; - - // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) - const cutoff = new Date('2026-02-19T00:00:00Z'); - const prCreated = new Date(pr.created_at); - if (prCreated < cutoff) { - console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); - return; - } - - // Check if author is a team member or bot - if (login === 'opencode-agent[bot]') return; - const { data: file } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: '.github/TEAM_MEMBERS', - ref: 'dev' - }); - const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean); - if (members.includes(login)) { - console.log(`Skipping: ${login} is a team member`); - return; - } - - const title = pr.title; - - async function addLabel(label) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: [label] - }); - } - - async function removeLabel(label) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - name: label - }); - } catch (e) { - // Label wasn't present, ignore - } - } - - async function comment(marker, body) { - const markerText = ``; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - - const existing = comments.find(c => c.body.includes(markerText)); - if (existing) return; - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: markerText + '\n' + body - }); - } - - // Step 1: Check title format - // Matches: feat:, feat(scope):, feat (scope):, etc. - const titlePattern = /^(feat|fix|docs|chore|refactor|test)\s*(\([a-zA-Z0-9-]+\))?\s*:/; - const hasValidTitle = titlePattern.test(title); - - if (!hasValidTitle) { - await addLabel('needs:title'); - await comment('title', `Hey! Your PR title \`${title}\` doesn't follow conventional commit format. - - Please update it to start with one of: - - \`feat:\` or \`feat(scope):\` new feature - - \`fix:\` or \`fix(scope):\` bug fix - - \`docs:\` or \`docs(scope):\` documentation changes - - \`chore:\` or \`chore(scope):\` maintenance tasks - - \`refactor:\` or \`refactor(scope):\` code refactoring - - \`test:\` or \`test(scope):\` adding or updating tests - - Where \`scope\` is the package name (e.g., \`app\`, \`desktop\`, \`opencode\`). - - See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for details.`); - return; - } - - await removeLabel('needs:title'); - - // Step 2: Check for linked issue (skip for docs/refactor/feat PRs) - const skipIssueCheck = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); - if (skipIssueCheck) { - await removeLabel('needs:issue'); - console.log('Skipping issue check for docs/refactor/feat PR'); - return; - } - const query = ` - query($owner: String!, $repo: String!, $number: Int!) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - closingIssuesReferences(first: 1) { - totalCount - } - } - } - } - `; - - const result = await github.graphql(query, { - owner: context.repo.owner, - repo: context.repo.repo, - number: pr.number - }); - - const linkedIssues = result.repository.pullRequest.closingIssuesReferences.totalCount; - - if (linkedIssues === 0) { - await addLabel('needs:issue'); - await comment('issue', `Thanks for your contribution! - - This PR doesn't have a linked issue. All PRs must reference an existing issue. - - Please: - 1. Open an issue describing the bug/feature (if one doesn't exist) - 2. Add \`Fixes #\` or \`Closes #\` to this PR description - - See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#issue-first-policy) for details.`); - return; - } - - await removeLabel('needs:issue'); - console.log('PR meets all standards'); - - check-compliance: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - name: Check PR template compliance - uses: actions/github-script@v7 - with: - script: | - const pr = context.payload.pull_request; - const login = pr.user.login; - - // Skip PRs older than Feb 18, 2026 at 6PM EST (Feb 19, 2026 00:00 UTC) - const cutoff = new Date('2026-02-19T00:00:00Z'); - const prCreated = new Date(pr.created_at); - if (prCreated < cutoff) { - console.log(`Skipping: PR #${pr.number} was created before cutoff (${prCreated.toISOString()})`); - return; - } - - // Check if author is a team member or bot - if (login === 'opencode-agent[bot]') return; - const { data: file } = await github.rest.repos.getContent({ - owner: context.repo.owner, - repo: context.repo.repo, - path: '.github/TEAM_MEMBERS', - ref: 'dev' - }); - const members = Buffer.from(file.content, 'base64').toString().split('\n').map(l => l.trim()).filter(Boolean); - if (members.includes(login)) { - console.log(`Skipping: ${login} is a team member`); - return; - } - - const body = pr.body || ''; - const title = pr.title; - const isDocsRefactorOrFeat = /^(docs|refactor|feat)\s*(\([a-zA-Z0-9-]+\))?\s*:/.test(title); - - const issues = []; - - // Check: template sections exist - const hasWhatSection = /### What does this PR do\?/.test(body); - const hasTypeSection = /### Type of change/.test(body); - const hasVerifySection = /### How did you verify your code works\?/.test(body); - const hasChecklistSection = /### Checklist/.test(body); - const hasIssueSection = /### Issue for this PR/.test(body); - - if (!hasWhatSection || !hasTypeSection || !hasVerifySection || !hasChecklistSection || !hasIssueSection) { - issues.push('PR description is missing required template sections. Please use the [PR template](../blob/dev/.github/pull_request_template.md).'); - } - - // Check: "What does this PR do?" has real content (not just placeholder text) - if (hasWhatSection) { - const whatMatch = body.match(/### What does this PR do\?\s*\n([\s\S]*?)(?=###|$)/); - const whatContent = whatMatch ? whatMatch[1].trim() : ''; - const placeholder = 'Please provide a description of the issue'; - const onlyPlaceholder = whatContent.includes(placeholder) && whatContent.replace(placeholder, '').replace(/[*\s]/g, '').length < 20; - if (!whatContent || onlyPlaceholder) { - issues.push('"What does this PR do?" section is empty or only contains placeholder text. Please describe your changes.'); - } - } - - // Check: at least one "Type of change" checkbox is checked - if (hasTypeSection) { - const typeMatch = body.match(/### Type of change\s*\n([\s\S]*?)(?=###|$)/); - const typeContent = typeMatch ? typeMatch[1] : ''; - const hasCheckedBox = /- \[x\]/i.test(typeContent); - if (!hasCheckedBox) { - issues.push('No "Type of change" checkbox is checked. Please select at least one.'); - } - } - - // Check: issue reference (skip for docs/refactor/feat) - if (!isDocsRefactorOrFeat && hasIssueSection) { - const issueMatch = body.match(/### Issue for this PR\s*\n([\s\S]*?)(?=###|$)/); - const issueContent = issueMatch ? issueMatch[1].trim() : ''; - const hasIssueRef = /(closes|fixes|resolves)\s+#\d+/i.test(issueContent) || /#\d+/.test(issueContent); - if (!hasIssueRef) { - issues.push('No issue referenced. Please add `Closes #` linking to the relevant issue.'); - } - } - - // Check: "How did you verify" has content - if (hasVerifySection) { - const verifyMatch = body.match(/### How did you verify your code works\?\s*\n([\s\S]*?)(?=###|$)/); - const verifyContent = verifyMatch ? verifyMatch[1].trim() : ''; - if (!verifyContent) { - issues.push('"How did you verify your code works?" section is empty. Please explain how you tested.'); - } - } - - // Check: checklist boxes are checked - if (hasChecklistSection) { - const checklistMatch = body.match(/### Checklist\s*\n([\s\S]*?)(?=###|$)/); - const checklistContent = checklistMatch ? checklistMatch[1] : ''; - const unchecked = (checklistContent.match(/- \[ \]/g) || []).length; - const checked = (checklistContent.match(/- \[x\]/gi) || []).length; - if (checked < 2) { - issues.push('Not all checklist items are checked. Please confirm you have tested locally and have not included unrelated changes.'); - } - } - - // Helper functions - async function addLabel(label) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - labels: [label] - }); - } - - async function removeLabel(label) { - try { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - name: label - }); - } catch (e) {} - } - - const hasComplianceLabel = pr.labels.some(l => l.name === 'needs:compliance'); - - if (issues.length > 0) { - // Non-compliant - if (!hasComplianceLabel) { - await addLabel('needs:compliance'); - } - - const marker = ''; - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - const existing = comments.find(c => c.body.includes(marker)); - - const body_text = `${marker} - This PR doesn't fully meet our [contributing guidelines](../blob/dev/CONTRIBUTING.md) and [PR template](../blob/dev/.github/pull_request_template.md). - - **What needs to be fixed:** - ${issues.map(i => `- ${i}`).join('\n')} - - Please edit this PR description to address the above within **2 hours**, or it will be automatically closed. - - If you believe this was flagged incorrectly, please let a maintainer know.`; - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: body_text - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: body_text - }); - } - - console.log(`PR #${pr.number} is non-compliant: ${issues.join(', ')}`); - } else if (hasComplianceLabel) { - // Was non-compliant, now fixed - await removeLabel('needs:compliance'); - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number - }); - const marker = ''; - const existing = comments.find(c => c.body.includes(marker)); - if (existing) { - await github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id - }); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: 'Thanks for updating your PR! It now meets our contributing guidelines. :+1:' - }); - - console.log(`PR #${pr.number} is now compliant, label removed`); - } else { - console.log(`PR #${pr.number} is compliant`); - } diff --git a/.github/workflows/publish-github-action.yml b/.github/workflows/publish-github-action.yml deleted file mode 100644 index d2789373a34a..000000000000 --- a/.github/workflows/publish-github-action.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: publish-github-action - -on: - workflow_dispatch: - push: - tags: - - "github-v*.*.*" - - "!github-v1" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - -jobs: - publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - run: git fetch --force --tags - - - name: Publish - run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ./script/publish - working-directory: ./github diff --git a/.github/workflows/publish-vscode.yml b/.github/workflows/publish-vscode.yml deleted file mode 100644 index f49a10578072..000000000000 --- a/.github/workflows/publish-vscode.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: publish-vscode - -on: - workflow_dispatch: - push: - tags: - - "vscode-v*.*.*" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - -jobs: - publish: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: ./.github/actions/setup-bun - - - run: git fetch --force --tags - - run: bun install -g @vscode/vsce - - - name: Install extension dependencies - run: bun install - working-directory: ./sdks/vscode - - - name: Publish - run: | - ./script/publish - working-directory: ./sdks/vscode - env: - VSCE_PAT: ${{ secrets.VSCE_PAT }} - OPENVSX_TOKEN: ${{ secrets.OPENVSX_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 5f7ee96b90d1..000000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,490 +0,0 @@ -name: publish -run-name: "${{ format('release {0}', inputs.bump) }}" - -on: - push: - branches: - - ci - - dev - - beta - - snapshot-* - workflow_dispatch: - inputs: - bump: - description: "Bump major, minor, or patch" - required: false - type: choice - options: - - major - - minor - - patch - version: - description: "Override version (optional)" - required: false - type: string - -concurrency: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version || inputs.bump }} - -permissions: - id-token: write - contents: write - packages: write - -jobs: - version: - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'anomalyco/opencode' - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: ./.github/actions/setup-bun - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Install OpenCode - if: inputs.bump || inputs.version - run: bun i -g opencode-ai - - - id: version - run: | - ./script/version.ts - env: - GH_TOKEN: ${{ steps.committer.outputs.token }} - OPENCODE_BUMP: ${{ inputs.bump }} - OPENCODE_VERSION: ${{ inputs.version }} - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GH_REPO: ${{ (github.ref_name == 'beta' && 'anomalyco/opencode-beta') || github.repository }} - outputs: - version: ${{ steps.version.outputs.version }} - release: ${{ steps.version.outputs.release }} - tag: ${{ steps.version.outputs.tag }} - repo: ${{ steps.version.outputs.repo }} - - build-cli: - needs: version - runs-on: blacksmith-4vcpu-ubuntu-2404 - if: github.repository == 'anomalyco/opencode' - steps: - - uses: actions/checkout@v3 - with: - fetch-tags: true - - - uses: ./.github/actions/setup-bun - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Build - id: build - run: | - ./packages/opencode/script/build.ts ${{ (github.ref_name == 'beta' && '--sourcemaps') || '' }} - env: - OPENCODE_VERSION: ${{ needs.version.outputs.version }} - OPENCODE_RELEASE: ${{ needs.version.outputs.release }} - GH_REPO: ${{ needs.version.outputs.repo }} - GH_TOKEN: ${{ steps.committer.outputs.token }} - - - uses: actions/upload-artifact@v4 - with: - name: opencode-cli - path: | - packages/opencode/dist/opencode-darwin* - packages/opencode/dist/opencode-linux* - - - uses: actions/upload-artifact@v4 - with: - name: opencode-cli-windows - path: packages/opencode/dist/opencode-windows* - outputs: - version: ${{ needs.version.outputs.version }} - - sign-cli-windows: - needs: - - build-cli - - version - runs-on: blacksmith-4vcpu-windows-2025 - if: github.repository == 'anomalyco/opencode' - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - steps: - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v4 - with: - name: opencode-cli-windows - path: packages/opencode/dist - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Azure login - uses: azure/login@v2 - with: - client-id: ${{ env.AZURE_CLIENT_ID }} - tenant-id: ${{ env.AZURE_TENANT_ID }} - subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} - - - uses: azure/artifact-signing-action@v1 - with: - endpoint: ${{ env.AZURE_TRUSTED_SIGNING_ENDPOINT }} - signing-account-name: ${{ env.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - certificate-profile-name: ${{ env.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - files: | - ${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe - ${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe - ${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe - exclude-environment-credential: true - exclude-workload-identity-credential: true - exclude-managed-identity-credential: true - exclude-shared-token-cache-credential: true - exclude-visual-studio-credential: true - exclude-visual-studio-code-credential: true - exclude-azure-cli-credential: false - exclude-azure-powershell-credential: true - exclude-azure-developer-cli-credential: true - exclude-interactive-browser-credential: true - - - name: Verify Windows CLI signatures - shell: pwsh - run: | - $files = @( - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64\bin\opencode.exe", - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64\bin\opencode.exe", - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline\bin\opencode.exe" - ) - - foreach ($file in $files) { - $sig = Get-AuthenticodeSignature $file - if ($sig.Status -ne "Valid") { - throw "Invalid signature for ${file}: $($sig.Status)" - } - } - - - name: Repack Windows CLI archives - working-directory: packages/opencode/dist - shell: pwsh - run: | - Compress-Archive -Path "opencode-windows-arm64\bin\*" -DestinationPath "opencode-windows-arm64.zip" -Force - Compress-Archive -Path "opencode-windows-x64\bin\*" -DestinationPath "opencode-windows-x64.zip" -Force - Compress-Archive -Path "opencode-windows-x64-baseline\bin\*" -DestinationPath "opencode-windows-x64-baseline.zip" -Force - - - name: Upload signed Windows CLI release assets - if: needs.version.outputs.release != '' - shell: pwsh - env: - GH_TOKEN: ${{ steps.committer.outputs.token }} - run: | - gh release upload "v${{ needs.version.outputs.version }}" ` - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-arm64.zip" ` - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64.zip" ` - "${{ github.workspace }}\packages\opencode\dist\opencode-windows-x64-baseline.zip" ` - --clobber ` - --repo "${{ needs.version.outputs.repo }}" - - - uses: actions/upload-artifact@v4 - with: - name: opencode-cli-signed-windows - path: | - packages/opencode/dist/opencode-windows-arm64 - packages/opencode/dist/opencode-windows-x64 - packages/opencode/dist/opencode-windows-x64-baseline - - build-electron: - needs: - - build-cli - - version - if: github.repository == 'anomalyco/opencode' - continue-on-error: false - env: - AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} - AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }} - AZURE_TRUSTED_SIGNING_ENDPOINT: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }} - strategy: - fail-fast: false - matrix: - settings: - - host: macos-26-intel - target: x86_64-apple-darwin - platform_flag: --mac --x64 - bun_install_flags: --os=darwin --cpu=x64 - - host: macos-26 - target: aarch64-apple-darwin - platform_flag: --mac --arm64 - bun_install_flags: --os=darwin --cpu=arm64 - # github-hosted: blacksmith lacks ARM64 MSVC cross-compilation toolchain - - host: "windows-2025" - target: aarch64-pc-windows-msvc - platform_flag: --win --arm64 - - host: "blacksmith-4vcpu-windows-2025" - target: x86_64-pc-windows-msvc - platform_flag: --win - - host: "blacksmith-4vcpu-ubuntu-2404" - target: x86_64-unknown-linux-gnu - platform_flag: --linux - - host: "blacksmith-4vcpu-ubuntu-2404" - target: aarch64-unknown-linux-gnu - platform_flag: --linux - runs-on: ${{ matrix.settings.host }} - steps: - - uses: actions/checkout@v3 - - - uses: apple-actions/import-codesign-certs@v2 - if: runner.os == 'macOS' - with: - keychain: build - p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} - p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - - name: Setup Apple API Key - if: runner.os == 'macOS' - run: echo "${{ secrets.APPLE_API_KEY_PATH }}" > $RUNNER_TEMP/apple-api-key.p8 - - - uses: ./.github/actions/setup-bun - with: - install-flags: ${{ matrix.settings.bun_install_flags }} - - - name: Azure login - if: runner.os == 'Windows' - uses: azure/login@v2 - with: - client-id: ${{ env.AZURE_CLIENT_ID }} - tenant-id: ${{ env.AZURE_TENANT_ID }} - subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} - - - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Cache apt packages - if: contains(matrix.settings.host, 'ubuntu') - uses: actions/cache@v4 - with: - path: ~/apt-cache - key: ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron-${{ hashFiles('.github/workflows/publish.yml') }} - restore-keys: | - ${{ runner.os }}-${{ matrix.settings.target }}-apt-electron- - - - name: Install dependencies (ubuntu only) - if: contains(matrix.settings.host, 'ubuntu') - run: | - mkdir -p ~/apt-cache && chmod -R a+rw ~/apt-cache - sudo apt-get update - sudo apt-get install -y --no-install-recommends -o dir::cache::archives="$HOME/apt-cache" rpm - sudo chmod -R a+rw ~/apt-cache - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Prepare - run: bun ./scripts/prepare.ts - working-directory: packages/desktop - env: - OPENCODE_VERSION: ${{ needs.version.outputs.version }} - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }} - RUST_TARGET: ${{ matrix.settings.target }} - GH_TOKEN: ${{ github.token }} - GITHUB_RUN_ID: ${{ github.run_id }} - - - name: Build - run: bun run build - working-directory: packages/desktop - env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ vars.SENTRY_ORG }} - SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }} - SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }} - VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }} - VITE_SENTRY_ENVIRONMENT: ${{ (github.ref_name == 'beta' && 'beta') || 'production' }} - VITE_SENTRY_RELEASE: desktop@${{ needs.version.outputs.version }} - - - name: Package and publish - if: needs.version.outputs.release - run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish always --config electron-builder.config.ts - working-directory: packages/desktop - timeout-minutes: 60 - env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - GH_TOKEN: ${{ steps.committer.outputs.token }} - CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} - CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 - APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} - APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} - - - name: Package (no publish) - if: ${{ !needs.version.outputs.release }} - run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts - working-directory: packages/desktop - timeout-minutes: 60 - env: - OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} - - - name: Create and upload macOS .app.tar.gz - if: runner.os == 'macOS' && needs.version.outputs.release - working-directory: packages/desktop/dist - env: - GH_TOKEN: ${{ steps.committer.outputs.token }} - run: | - if [[ "${{ matrix.settings.target }}" == "x86_64-apple-darwin" ]]; then - APP_DIR="mac" - OUT_NAME="opencode-desktop-mac-x64.app.tar.gz" - elif [[ "${{ matrix.settings.target }}" == "aarch64-apple-darwin" ]]; then - APP_DIR="mac-arm64" - OUT_NAME="opencode-desktop-mac-arm64.app.tar.gz" - else - echo "Unknown macOS target: ${{ matrix.settings.target }}" - exit 1 - fi - APP_PATH=$(find "$APP_DIR" -maxdepth 1 -name "*.app" -type d | head -1) - if [ -z "$APP_PATH" ]; then - echo "No .app bundle found in $APP_DIR" - exit 1 - fi - tar -czf "$OUT_NAME" -C "$(dirname "$APP_PATH")" "$(basename "$APP_PATH")" - gh release upload "v${{ needs.version.outputs.version }}" "$OUT_NAME" --clobber --repo "${{ needs.version.outputs.repo }}" - - - name: Verify signed Windows Electron artifacts - if: runner.os == 'Windows' - shell: pwsh - run: | - $files = @() - $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*.exe" | Select-Object -ExpandProperty FullName - $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\*.exe" | Select-Object -ExpandProperty FullName - $files += Get-ChildItem "${{ github.workspace }}\packages\desktop\dist\*unpacked\resources\opencode-cli.exe" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName - - foreach ($file in $files | Select-Object -Unique) { - $sig = Get-AuthenticodeSignature $file - if ($sig.Status -ne "Valid") { - throw "Invalid signature for ${file}: $($sig.Status)" - } - } - - - uses: actions/upload-artifact@v4 - with: - name: opencode-desktop-${{ matrix.settings.target }} - path: packages/desktop/dist/* - - - uses: actions/upload-artifact@v4 - if: needs.version.outputs.release - with: - name: latest-yml-${{ matrix.settings.target }} - path: packages/desktop/dist/latest*.yml - - publish: - needs: - - version - - build-cli - - sign-cli-windows - - build-electron - if: always() && !failure() && !cancelled() - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v3 - - - uses: ./.github/actions/setup-bun - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - uses: actions/setup-node@v4 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - - - uses: actions/download-artifact@v4 - with: - name: opencode-cli - path: packages/opencode/dist - - - uses: actions/download-artifact@v4 - with: - name: opencode-cli-windows - path: packages/opencode/dist - - - uses: actions/download-artifact@v4 - with: - name: opencode-cli-signed-windows - path: packages/opencode/dist - - - uses: actions/download-artifact@v4 - if: needs.version.outputs.release - with: - pattern: latest-yml-* - path: /tmp/latest-yml - - - name: Setup git committer - id: committer - uses: ./.github/actions/setup-git-committer - with: - opencode-app-id: ${{ vars.OPENCODE_APP_ID }} - opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} - - - name: Cache apt packages (AUR) - uses: actions/cache@v4 - with: - path: /var/cache/apt/archives - key: ${{ runner.os }}-apt-aur-${{ hashFiles('.github/workflows/publish.yml') }} - restore-keys: | - ${{ runner.os }}-apt-aur- - - - name: Setup SSH for AUR - run: | - sudo apt-get update - sudo apt-get install -y pacman-package-manager - mkdir -p ~/.ssh - echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts || true - - - run: ./script/publish.ts - env: - OPENCODE_VERSION: ${{ needs.version.outputs.version }} - OPENCODE_RELEASE: ${{ needs.version.outputs.release }} - AUR_KEY: ${{ secrets.AUR_KEY }} - GITHUB_TOKEN: ${{ steps.committer.outputs.token }} - GH_REPO: ${{ needs.version.outputs.repo }} - NPM_CONFIG_PROVENANCE: false - LATEST_YML_DIR: /tmp/latest-yml - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} diff --git a/.github/workflows/release-github-action.yml b/.github/workflows/release-github-action.yml deleted file mode 100644 index 3f5caa55c8dc..000000000000 --- a/.github/workflows/release-github-action.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: release-github-action - -on: - push: - branches: - - dev - paths: - - "github/**" - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -permissions: - contents: write - -jobs: - release: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - run: git fetch --force --tags - - - name: Release - run: | - git config --global user.email "opencode@sst.dev" - git config --global user.name "opencode" - ./github/script/release diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 2bd1f0c4a002..000000000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: review - -on: - issue_comment: - types: [created] - -jobs: - check-guidelines: - if: | - github.event.issue.pull_request && - startsWith(github.event.comment.body, '/review') && - contains(fromJson('["OWNER","MEMBER"]'), github.event.comment.author_association) - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - pull-requests: write - steps: - - name: Get PR number - id: pr-number - run: | - if [ "${{ github.event_name }}" = "pull_request_target" ]; then - echo "number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT - else - echo "number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT - fi - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Get PR details - id: pr-details - run: | - gh api /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }} > pr_data.json - echo "title=$(jq -r .title pr_data.json)" >> $GITHUB_OUTPUT - echo "sha=$(jq -r .head.sha pr_data.json)" >> $GITHUB_OUTPUT - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check PR guidelines compliance - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }' - PR_TITLE: ${{ steps.pr-details.outputs.title }} - run: | - PR_BODY=$(jq -r .body pr_data.json) - opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}' - - - ${{ steps.pr-number.outputs.number }} - - - - $PR_BODY - - - Please check all the code changes in this pull request against the style guide, also look for any bugs if they exist. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do - - When critiquing code against the style guide, be sure that the code is ACTUALLY in violation, don't complain about else statements if they already use early returns there. You may complain about excessive nesting though, regardless of else statement usage. - When critiquing code style don't be a zealot, we don't like "let" statements but sometimes they are the simplest option, if someone does a bunch of nesting with let, they should consider using iife (see packages/opencode/src/util.iife.ts) - - Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block. - If you are writing suggested fixes, BE SURE THAT the change you are recommending is actually valid typescript, often I have seen missing closing "}" or other syntax errors. - Generally, write a comment instead of writing suggested change if you can help it. - - Command MUST be like this. - \`\`\` - gh api \ - --method POST \ - -H \"Accept: application/vnd.github+json\" \ - -H \"X-GitHub-Api-Version: 2022-11-28\" \ - /repos/${{ github.repository }}/pulls/${{ steps.pr-number.outputs.number }}/comments \ - -f 'body=[summary of issue]' -f 'commit_id=${{ steps.pr-details.outputs.sha }}' -f 'path=[path-to-file]' -F \"line=[line]\" -f 'side=RIGHT' - \`\`\` - - Only create comments for actual violations. If the code follows all guidelines, comment on the issue using gh cli: 'lgtm' AND NOTHING ELSE!!!!." diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml deleted file mode 100644 index 824733901d6b..000000000000 --- a/.github/workflows/stats.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: stats - -on: - schedule: - - cron: "0 12 * * *" # Run daily at 12:00 UTC - workflow_dispatch: # Allow manual trigger - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - stats: - if: github.repository == 'anomalyco/opencode' - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Run stats script - run: bun script/stats.ts - - - name: Commit stats - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - git add STATS.md - git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)" - git push - env: - POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }} diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml deleted file mode 100644 index 6d143a8a22f4..000000000000 --- a/.github/workflows/storybook.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: storybook - -on: - push: - branches: [dev] - paths: - - ".github/workflows/storybook.yml" - - "package.json" - - "bun.lock" - - "packages/storybook/**" - - "packages/ui/**" - pull_request: - branches: [dev] - paths: - - ".github/workflows/storybook.yml" - - "package.json" - - "bun.lock" - - "packages/storybook/**" - - "packages/ui/**" - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: storybook build - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Build Storybook - run: bun --cwd packages/storybook build diff --git a/.github/workflows/sync-zed-extension.yml b/.github/workflows/sync-zed-extension.yml deleted file mode 100644 index f14487cde974..000000000000 --- a/.github/workflows/sync-zed-extension.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: "sync-zed-extension" - -on: - workflow_dispatch: - release: - types: [published] - -jobs: - zed: - name: Release Zed Extension - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: ./.github/actions/setup-bun - - - name: Get version tag - id: get_tag - run: | - if [ "${{ github.event_name }}" = "release" ]; then - TAG="${{ github.event.release.tag_name }}" - else - TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1) - fi - echo "tag=${TAG}" >> $GITHUB_OUTPUT - echo "Using tag: ${TAG}" - - - name: Sync Zed extension - run: | - ./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }} - env: - ZED_EXTENSIONS_PAT: ${{ secrets.ZED_EXTENSIONS_PAT }} - ZED_PR_PAT: ${{ secrets.ZED_PR_PAT }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index f226d3483a71..000000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: test - -on: - push: - branches: - - dev - pull_request: - workflow_dispatch: - -concurrency: - # Keep every run on dev so cancelled checks do not pollute the default branch - # commit history. PRs and other branches still share a group and cancel stale runs. - group: ${{ case(github.ref == 'refs/heads/dev', format('{0}-{1}', github.workflow, github.run_id), format('{0}-{1}', github.workflow, github.event.pull_request.number || github.ref)) }} - cancel-in-progress: true - -permissions: - contents: read - checks: write - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - unit: - name: unit (${{ matrix.settings.name }}) - strategy: - fail-fast: false - matrix: - settings: - - name: linux - host: blacksmith-4vcpu-ubuntu-2404 - - name: windows - host: blacksmith-4vcpu-windows-2025 - runs-on: ${{ matrix.settings.host }} - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Configure git identity - run: | - git config --global user.email "bot@opencode.ai" - git config --global user.name "opencode" - - - name: Cache Turbo - uses: actions/cache@v4 - with: - path: node_modules/.cache/turbo - key: turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}-${{ github.sha }} - restore-keys: | - turbo-${{ runner.os }}-${{ hashFiles('turbo.json', '**/package.json') }}- - turbo-${{ runner.os }}- - - - name: Run unit tests - run: bun turbo test:ci - env: - OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} - - - name: Run HttpApi exerciser gates - if: runner.os == 'Linux' - working-directory: packages/opencode - run: bun run test:httpapi - - - name: Publish unit reports - if: always() - uses: mikepenz/action-junit-report@v6 - with: - report_paths: packages/*/.artifacts/unit/junit.xml - check_name: "unit results (${{ matrix.settings.name }})" - detailed_summary: true - include_time_in_summary: true - fail_on_failure: false - - - name: Upload unit artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: unit-${{ matrix.settings.name }}-${{ github.run_attempt }} - include-hidden-files: true - if-no-files-found: ignore - retention-days: 7 - path: packages/*/.artifacts/unit/junit.xml - - e2e: - name: e2e (${{ matrix.settings.name }}) - strategy: - fail-fast: false - matrix: - settings: - - name: linux - host: blacksmith-4vcpu-ubuntu-2404 - - name: windows - host: blacksmith-4vcpu-windows-2025 - runs-on: ${{ matrix.settings.host }} - env: - PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Read Playwright version - id: playwright-version - run: | - version=$(node -e 'console.log(require("./package.json").workspaces.catalog["@playwright/test"])') - echo "version=$version" >> "$GITHUB_OUTPUT" - - - name: Cache Playwright browsers - id: playwright-cache - uses: actions/cache@v4 - with: - path: ${{ github.workspace }}/.playwright-browsers - key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium - - - name: Install Playwright system dependencies - if: runner.os == 'Linux' - working-directory: packages/app - run: bunx playwright install-deps chromium - - - name: Install Playwright browsers - if: steps.playwright-cache.outputs.cache-hit != 'true' - working-directory: packages/app - run: bunx playwright install chromium - - - name: Run app e2e tests - run: bun --cwd packages/app test:e2e:local - env: - CI: true - PLAYWRIGHT_JUNIT_OUTPUT: e2e/junit-${{ matrix.settings.name }}.xml - timeout-minutes: 30 - - - name: Upload Playwright artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: playwright-${{ matrix.settings.name }}-${{ github.run_attempt }} - if-no-files-found: ignore - retention-days: 7 - path: | - packages/app/e2e/junit-*.xml - packages/app/e2e/test-results - packages/app/e2e/playwright-report diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml deleted file mode 100644 index 99e7b5b34fe3..000000000000 --- a/.github/workflows/triage.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: triage - -on: - issues: - types: [opened] - -jobs: - triage: - runs-on: blacksmith-4vcpu-ubuntu-2404 - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Install opencode - run: curl -fsSL https://opencode.ai/install | bash - - - name: Triage issue - env: - OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} - ISSUE_TITLE: ${{ github.event.issue.title }} - ISSUE_BODY: ${{ github.event.issue.body }} - run: | - opencode run --agent triage "The following issue was just opened, triage it: - - Title: $ISSUE_TITLE - - $ISSUE_BODY" diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml deleted file mode 100644 index b247d24b40db..000000000000 --- a/.github/workflows/typecheck.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: typecheck - -on: - push: - branches: [dev] - pull_request: - branches: [dev] - workflow_dispatch: - -jobs: - typecheck: - runs-on: blacksmith-4vcpu-ubuntu-2404 - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Bun - uses: ./.github/actions/setup-bun - - - name: Run typecheck - run: bun typecheck diff --git a/README.md b/README.md index ccce3e97bb92..e4d1f3fab6f9 100644 --- a/README.md +++ b/README.md @@ -1,141 +1 @@ -

- - - - - OpenCode logo - - -

-

The open source AI coding agent.

-

- Discord - npm - Build status -

- -

- English | - 简体中文 | - 繁體中文 | - 한국어 | - Deutsch | - Español | - Français | - Italiano | - Dansk | - 日本語 | - Polski | - Русский | - Bosanski | - العربية | - Norsk | - Português (Brasil) | - ไทย | - Türkçe | - Українська | - বাংলা | - Ελληνικά | - Tiếng Việt -

- -[![OpenCode Terminal UI](packages/web/src/assets/lander/screenshot.png)](https://opencode.ai) - ---- - -### Installation - -```bash -# YOLO -curl -fsSL https://opencode.ai/install | bash - -# Package managers -npm i -g opencode-ai@latest # or bun/pnpm/yarn -scoop install opencode # Windows -choco install opencode # Windows -brew install anomalyco/tap/opencode # macOS and Linux (recommended, always up to date) -brew install opencode # macOS and Linux (official brew formula, updated less) -sudo pacman -S opencode # Arch Linux (Stable) -paru -S opencode-bin # Arch Linux (Latest from AUR) -mise use -g opencode # Any OS -nix run nixpkgs#opencode # or github:anomalyco/opencode for latest dev branch -``` - -> [!TIP] -> Remove versions older than 0.1.x before installing. - -### Desktop App (BETA) - -OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/anomalyco/opencode/releases) or [opencode.ai/download](https://opencode.ai/download). - -| Platform | Download | -| --------------------- | ---------------------------------- | -| macOS (Apple Silicon) | `opencode-desktop-mac-arm64.dmg` | -| macOS (Intel) | `opencode-desktop-mac-x64.dmg` | -| Windows | `opencode-desktop-windows-x64.exe` | -| Linux | `.deb`, `.rpm`, or `.AppImage` | - -```bash -# macOS (Homebrew) -brew install --cask opencode-desktop -# Windows (Scoop) -scoop bucket add extras; scoop install extras/opencode-desktop -``` - -#### Installation Directory - -The install script respects the following priority order for the installation path: - -1. `$OPENCODE_INSTALL_DIR` - Custom installation directory -2. `$XDG_BIN_DIR` - XDG Base Directory Specification compliant path -3. `$HOME/bin` - Standard user binary directory (if it exists or can be created) -4. `$HOME/.opencode/bin` - Default fallback - -```bash -# Examples -OPENCODE_INSTALL_DIR=/usr/local/bin curl -fsSL https://opencode.ai/install | bash -XDG_BIN_DIR=$HOME/.local/bin curl -fsSL https://opencode.ai/install | bash -``` - -### Agents - -OpenCode includes two built-in agents you can switch between with the `Tab` key. - -- **build** - Default, full-access agent for development work -- **plan** - Read-only agent for analysis and code exploration - - Denies file edits by default - - Asks permission before running bash commands - - Ideal for exploring unfamiliar codebases or planning changes - -Also included is a **general** subagent for complex searches and multistep tasks. -This is used internally and can be invoked using `@general` in messages. - -Learn more about [agents](https://opencode.ai/docs/agents). - -### Documentation - -For more info on how to configure OpenCode, [**head over to our docs**](https://opencode.ai/docs). - -### Contributing - -If you're interested in contributing to OpenCode, please read our [contributing docs](./CONTRIBUTING.md) before submitting a pull request. - -### Building on OpenCode - -If you are working on a project that's related to OpenCode and is using "opencode" as part of its name, for example "opencode-dashboard" or "opencode-mobile", please add a note to your README to clarify that it is not built by the OpenCode team and is not affiliated with us in any way. - -### FAQ - -#### How is this different from Claude Code? - -It's very similar to Claude Code in terms of capability. Here are the key differences: - -- 100% open source -- Not coupled to any provider. Although we recommend the models we provide through [OpenCode Zen](https://opencode.ai/zen), OpenCode can be used with Claude, OpenAI, Google, or even local models. As models evolve, the gaps between them will close and pricing will drop, so being provider-agnostic is important. -- Built-in opt-in LSP support -- A focus on TUI. OpenCode is built by neovim users and the creators of [terminal.shop](https://terminal.shop); we are going to push the limits of what's possible in the terminal. -- A client/server architecture. This, for example, can allow OpenCode to run on your computer while you drive it remotely from a mobile app, meaning that the TUI frontend is just one of the possible clients. - ---- - -**Join our community** [Discord](https://discord.gg/opencode) | [X.com](https://x.com/opencode) +trying to run the last workflow in github action with another attempt diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts index 986008c4f4f1..60dfe3d69661 100644 --- a/packages/desktop/electron-builder.config.ts +++ b/packages/desktop/electron-builder.config.ts @@ -62,7 +62,7 @@ const getBase = (): Configuration => ({ signtoolOptions: { sign: signWindows, }, - target: ["nsis"], + target: ["nsis", "portable"], verifyUpdateCodeSignature: false, }, nsis: { @@ -71,6 +71,9 @@ const getBase = (): Configuration => ({ installerIcon: `resources/icons/icon.ico`, installerHeaderIcon: `resources/icons/icon.ico`, }, + portable: { + artifactName: "opencode-desktop-${os}-${arch}-portable.${ext}", + }, linux: { icon: `resources/icons`, category: "Development", diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index 1b624800e8e0..80482f9cf139 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -4,7 +4,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs" import * as http from "node:http" import { createServer } from "node:net" import { homedir, tmpdir } from "node:os" -import { join } from "node:path" +import { dirname, join } from "node:path" import { getCACertificates, setDefaultCACertificates } from "node:tls" import type { Event } from "electron" import { app, BrowserWindow } from "electron" @@ -164,7 +164,13 @@ const main = Effect.gen(function* () { return } - preferAppEnv(app.getPath("userData")) + const dataPath = app.isPackaged + ? join(dirname(app.getPath("exe")), "data") + : app.getPath("userData") + if (app.isPackaged && !existsSync(dataPath)) { + mkdirSync(dataPath, { recursive: true }) + } + preferAppEnv(dataPath) app.on("second-instance", (_event: Event, argv: string[]) => { const urls = argv.filter((arg: string) => arg.startsWith("opencode://")) @@ -303,7 +309,7 @@ const main = Effect.gen(function* () { }, { needsMigration, - userDataPath: app.getPath("userData"), + userDataPath: dataPath, onSqliteProgress: (progress) => initEmitter.emit("sqlite", progress), onStdout: (message) => logger.log("sidecar stdout", { message }), onStderr: (message) => logger.warn("sidecar stderr", { message }), diff --git a/packages/desktop/src/main/logging.ts b/packages/desktop/src/main/logging.ts index 5d373ed27fbd..2d79658c6806 100644 --- a/packages/desktop/src/main/logging.ts +++ b/packages/desktop/src/main/logging.ts @@ -1,6 +1,7 @@ import { MainLogger } from "electron-log" import log from "electron-log/main.js" -import { readFileSync, readdirSync, statSync, unlinkSync } from "node:fs" +import { app } from "electron" +import { mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, existsSync } from "node:fs" import { dirname, join } from "node:path" const MAX_LOG_AGE_DAYS = 7 @@ -9,7 +10,22 @@ const TAIL_LINES = 1000 let logger: MainLogger export const getLogger = () => logger +function getPortableLogPath(): string { + if (app.isPackaged) { + const exePath = app.getPath("exe") + const exeDir = dirname(exePath) + const logDir = join(exeDir, "data", "logs") + if (!existsSync(logDir)) { + mkdirSync(logDir, { recursive: true }) + } + return logDir + } + return log.transports.file.getFile().path.replace(/[^\\\/]+$/, "") +} + export function initLogging() { + const logDir = getPortableLogPath() + log.transports.file.resolvePathFn = () => join(logDir, "opencode.log") log.transports.file.maxSize = 5 * 1024 * 1024 initConsoleTransport() cleanup() diff --git a/packages/desktop/src/main/store.ts b/packages/desktop/src/main/store.ts index a591f878decf..84d4e94dbd9f 100644 --- a/packages/desktop/src/main/store.ts +++ b/packages/desktop/src/main/store.ts @@ -1,20 +1,26 @@ import Store from "electron-store" import { app } from "electron" +import { dirname, join } from "node:path" import { SETTINGS_STORE } from "./constants" const cache = new Map() -// We cannot instantiate the electron-store at module load time because -// module import hoisting causes this to run before app.setPath("userData", ...) -// in index.ts has executed, which would result in files being written to the default directory -// (e.g. bad: %APPDATA%\@opencode-ai\desktop\opencode.settings vs good: %APPDATA%\ai.opencode.desktop.dev\opencode.settings). +function getPortableDataPath(): string { + if (app.isPackaged) { + const exePath = app.getPath("exe") + const exeDir = dirname(exePath) + return join(exeDir, "data") + } + return app.getPath("userData") +} + export function getStore(name = SETTINGS_STORE) { const cached = cache.get(name) if (cached) return cached const next = new Store({ name, - cwd: app.getPath("userData"), + cwd: getPortableDataPath(), fileExtension: "", accessPropertiesByDotNotation: false, })