Update Opencode Commit #125
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update Opencode Commit | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 1-23/2 * * *" | |
| permissions: | |
| contents: write | |
| issues: write # Required for creating issues on CI failure | |
| jobs: | |
| update-opencode-commit: | |
| name: Update Opencode Commit | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check for existing automated submodule failure issue | |
| id: failure_issue_gate | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const failureMarker = 'Automated submodule update failed'; | |
| const openAutomatedIssues = await github.paginate( | |
| github.rest.issues.listForRepo, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'automated', | |
| per_page: 100, | |
| }, | |
| ); | |
| const matchingIssues = openAutomatedIssues.filter((issue) => | |
| (issue.body || '').includes(failureMarker), | |
| ); | |
| matchingIssues.sort((a, b) => b.number - a.number); | |
| const existingIssue = matchingIssues[0] || null; | |
| const hasOpenFailureIssue = Boolean(existingIssue); | |
| const shouldBlock = | |
| context.eventName === 'schedule' && hasOpenFailureIssue; | |
| core.info( | |
| `failure_issue_gate: event=${context.eventName}, has_open_failure_issue=${hasOpenFailureIssue}, should_block=${shouldBlock}`, | |
| ); | |
| if (existingIssue) { | |
| core.info( | |
| `failure_issue_gate: using #${existingIssue.number} ${existingIssue.html_url}`, | |
| ); | |
| } | |
| core.setOutput('has_open_failure_issue', String(hasOpenFailureIssue)); | |
| core.setOutput( | |
| 'existing_failure_issue_number', | |
| existingIssue ? String(existingIssue.number) : '', | |
| ); | |
| core.setOutput( | |
| 'existing_failure_issue_url', | |
| existingIssue ? existingIssue.html_url : '', | |
| ); | |
| core.setOutput( | |
| 'existing_failure_issue_title', | |
| existingIssue ? existingIssue.title : '', | |
| ); | |
| core.setOutput('should_block', String(shouldBlock)); | |
| - name: Initialize opencode submodule | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| run: | | |
| # .gitmodules tracks opencode with an SSH URL. Actions runners | |
| # typically lack SSH deploy keys, so rewrite to HTTPS inline. | |
| git -c url."https://github.com/".insteadOf=git@github.com: \ | |
| -c url."https://github.com/".insteadOf=ssh://git@github.com/ \ | |
| submodule update --init --recursive packages/opencode | |
| git submodule status --recursive | |
| - name: Configure Git | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| # Toolchain setup for running CI checks on the updated submodule. | |
| # Steps match ci-pre-publish.yml to ensure parity with normal CI. | |
| - name: Install Linux system dependencies | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| run: | | |
| # Required by opencode-broker (libpam-sys links against -lpam). | |
| sudo apt-get update | |
| sudo apt-get install -y libpam0g-dev | |
| - name: Install Rust | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: rustfmt, clippy | |
| - name: Install just | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| uses: extractions/setup-just@v3 | |
| - name: Setup Node.js | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: '20' | |
| - name: Setup Bun | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| uses: ./.github/actions/setup-bun | |
| - name: Rust cache | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| uses: Swatinem/rust-cache@v2 | |
| - name: Update opencode commit | |
| id: update | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| run: | | |
| ./scripts/update-opencode-commit.sh | |
| # Validate the updated submodule before committing. | |
| # Auto-fix formatting first, then run the full CI check suite. | |
| # If checks fail, the workflow fails and the broken commit is never pushed. | |
| - name: Auto-format and run CI checks | |
| id: ci_checks | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' && steps.update.outputs.needs_update == 'true' | |
| run: | | |
| set -euo pipefail | |
| # Auto-fix formatting issues introduced by the submodule update | |
| just fmt || true | |
| # If formatting produced changes, stage and commit them | |
| if ! git diff --quiet; then | |
| git add -A | |
| git commit -m "style: auto-format after opencode submodule update" || true | |
| fi | |
| # Run the full CI check suite | |
| just ci-checks | |
| - name: Run e2e tests | |
| id: e2e_checks | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' && steps.update.outputs.needs_update == 'true' | |
| run: just ci-e2e | |
| timeout-minutes: 30 | |
| - name: Verify OCI description label | |
| id: verify_oci | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| run: python3 scripts/extract-oci-description.py packages/core/src/docker/Dockerfile | |
| - name: Commit and push if changed | |
| id: commit_push | |
| if: steps.failure_issue_gate.outputs.should_block != 'true' | |
| run: | | |
| if git diff --quiet; then | |
| echo "No changes to commit." | |
| { | |
| echo "changed=false" | |
| echo "pushed=false" | |
| echo "commit_sha=" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Keep superproject updates atomic: commit the submodule pointer and | |
| # any root bun.lock refresh together when they change in the same run. | |
| git add bun.lock packages/core/src/docker/Dockerfile packages/opencode | |
| git commit -m "chore: update opencode commit" | |
| # Fail fast before push if the staged submodule gitlink points to a | |
| # commit that is not yet fetchable from the submodule remote. | |
| ./scripts/check-opencode-submodule-published.sh --from-index | |
| commit_sha="$(git rev-parse HEAD)" | |
| { | |
| echo "changed=true" | |
| echo "pushed=false" | |
| echo "commit_sha=${commit_sha}" | |
| } >> "$GITHUB_OUTPUT" | |
| git push | |
| echo "pushed=true" >> "$GITHUB_OUTPUT" | |
| - name: Notice skipped update due to existing open failure issue | |
| if: steps.failure_issue_gate.outputs.should_block == 'true' | |
| env: | |
| EXISTING_FAILURE_ISSUE_NUMBER: ${{ steps.failure_issue_gate.outputs.existing_failure_issue_number }} | |
| EXISTING_FAILURE_ISSUE_URL: ${{ steps.failure_issue_gate.outputs.existing_failure_issue_url }} | |
| EXISTING_FAILURE_ISSUE_TITLE: ${{ steps.failure_issue_gate.outputs.existing_failure_issue_title }} | |
| run: | | |
| set -euo pipefail | |
| echo "::notice title=Update skipped::Scheduled run skipped because an open failure issue already exists (#${EXISTING_FAILURE_ISSUE_NUMBER}): ${EXISTING_FAILURE_ISSUE_TITLE} (${EXISTING_FAILURE_ISSUE_URL})" | |
| - name: Write summary | |
| if: always() | |
| env: | |
| FAILURE_ISSUE_GATE_OUTCOME: ${{ steps.failure_issue_gate.outcome }} | |
| HAS_OPEN_FAILURE_ISSUE: ${{ steps.failure_issue_gate.outputs.has_open_failure_issue }} | |
| EXISTING_FAILURE_ISSUE_NUMBER: ${{ steps.failure_issue_gate.outputs.existing_failure_issue_number }} | |
| EXISTING_FAILURE_ISSUE_URL: ${{ steps.failure_issue_gate.outputs.existing_failure_issue_url }} | |
| EXISTING_FAILURE_ISSUE_TITLE: ${{ steps.failure_issue_gate.outputs.existing_failure_issue_title }} | |
| SHOULD_BLOCK: ${{ steps.failure_issue_gate.outputs.should_block }} | |
| UPDATE_OUTCOME: ${{ steps.update.outcome }} | |
| CI_CHECKS_OUTCOME: ${{ steps.ci_checks.outcome }} | |
| E2E_CHECKS_OUTCOME: ${{ steps.e2e_checks.outcome }} | |
| VERIFY_OCI_OUTCOME: ${{ steps.verify_oci.outcome }} | |
| COMMIT_PUSH_OUTCOME: ${{ steps.commit_push.outcome }} | |
| SUBMODULE_BRANCH: ${{ steps.update.outputs.submodule_branch }} | |
| SUBMODULE_REPO_URL: ${{ steps.update.outputs.submodule_repo_url }} | |
| CURRENT_SUBMODULE_COMMIT: ${{ steps.update.outputs.current_submodule_commit }} | |
| LATEST_COMMIT: ${{ steps.update.outputs.latest_commit }} | |
| CURRENT_PIN_VALUE: ${{ steps.update.outputs.current_pin_value }} | |
| UPDATED_PIN_VALUE: ${{ steps.update.outputs.updated_pin_value }} | |
| NEEDS_UPDATE: ${{ steps.update.outputs.needs_update }} | |
| UPDATED: ${{ steps.update.outputs.updated }} | |
| CHANGED: ${{ steps.commit_push.outputs.changed }} | |
| PUSHED: ${{ steps.commit_push.outputs.pushed }} | |
| COMMIT_SHA: ${{ steps.commit_push.outputs.commit_sha }} | |
| run: | | |
| set -euo pipefail | |
| fallback() { | |
| local value="${1:-}" | |
| if [[ -z "${value}" ]]; then | |
| echo "n/a" | |
| else | |
| echo "${value}" | |
| fi | |
| } | |
| branch="$(fallback "${SUBMODULE_BRANCH:-}")" | |
| repo_url="$(fallback "${SUBMODULE_REPO_URL:-}")" | |
| current_submodule="$(fallback "${CURRENT_SUBMODULE_COMMIT:-}")" | |
| latest_submodule="$(fallback "${LATEST_COMMIT:-}")" | |
| current_pin="$(fallback "${CURRENT_PIN_VALUE:-}")" | |
| updated_pin="$(fallback "${UPDATED_PIN_VALUE:-}")" | |
| needs_update="$(fallback "${NEEDS_UPDATE:-}")" | |
| updated="$(fallback "${UPDATED:-}")" | |
| changed="$(fallback "${CHANGED:-}")" | |
| pushed="$(fallback "${PUSHED:-}")" | |
| has_open_failure_issue="$(fallback "${HAS_OPEN_FAILURE_ISSUE:-}")" | |
| should_block="$(fallback "${SHOULD_BLOCK:-}")" | |
| existing_failure_issue_number="$(fallback "${EXISTING_FAILURE_ISSUE_NUMBER:-}")" | |
| existing_failure_issue_url="$(fallback "${EXISTING_FAILURE_ISSUE_URL:-}")" | |
| existing_failure_issue_title="$(fallback "${EXISTING_FAILURE_ISSUE_TITLE:-}")" | |
| superproject_repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" | |
| commit_link="n/a" | |
| if [[ "${PUSHED:-}" == "true" && -n "${COMMIT_SHA:-}" ]]; then | |
| commit_link="${superproject_repo_url}/commit/${COMMIT_SHA}" | |
| fi | |
| compare_link="n/a" | |
| if [[ -n "${SUBMODULE_REPO_URL:-}" && -n "${CURRENT_SUBMODULE_COMMIT:-}" && -n "${LATEST_COMMIT:-}" && "${CURRENT_SUBMODULE_COMMIT}" != "${LATEST_COMMIT}" ]]; then | |
| compare_link="${SUBMODULE_REPO_URL}/compare/${CURRENT_SUBMODULE_COMMIT}...${LATEST_COMMIT}" | |
| fi | |
| status_line="ℹ️ No update needed" | |
| if [[ "${SHOULD_BLOCK:-}" == "true" ]]; then | |
| status_line="⏭️ Skipped due to open failure issue" | |
| elif [[ "${UPDATE_OUTCOME:-}" == "failure" || "${CI_CHECKS_OUTCOME:-}" == "failure" || "${E2E_CHECKS_OUTCOME:-}" == "failure" || "${VERIFY_OCI_OUTCOME:-}" == "failure" || "${COMMIT_PUSH_OUTCOME:-}" == "failure" || "${UPDATE_OUTCOME:-}" == "cancelled" || "${CI_CHECKS_OUTCOME:-}" == "cancelled" || "${E2E_CHECKS_OUTCOME:-}" == "cancelled" || "${VERIFY_OCI_OUTCOME:-}" == "cancelled" || "${COMMIT_PUSH_OUTCOME:-}" == "cancelled" ]]; then | |
| status_line="❌ Failed" | |
| elif [[ "${PUSHED:-}" == "true" ]]; then | |
| status_line="✅ Updated and pushed" | |
| fi | |
| { | |
| echo "## ${status_line}" | |
| echo "" | |
| echo "**Trigger:** \`${GITHUB_EVENT_NAME}\`" | |
| echo "**Ref:** \`${GITHUB_REF}\`" | |
| echo "**Actor:** \`${GITHUB_ACTOR}\`" | |
| echo "" | |
| echo "**Submodule branch:** \`${branch}\`" | |
| echo "**Submodule repo:** ${repo_url}" | |
| echo "" | |
| echo "**Submodule commit:** \`${current_submodule} -> ${latest_submodule}\`" | |
| echo "**Dockerfile pin:** \`${current_pin} -> ${updated_pin}\`" | |
| echo "" | |
| echo "**Update flags:**" | |
| echo "- has_open_failure_issue: \`${has_open_failure_issue}\`" | |
| echo "- should_block: \`${should_block}\`" | |
| echo "- needs_update: \`${needs_update}\`" | |
| echo "- updated: \`${updated}\`" | |
| echo "- changed: \`${changed}\`" | |
| echo "- pushed: \`${pushed}\`" | |
| echo "" | |
| echo "**Links:**" | |
| if [[ "${compare_link}" == "n/a" ]]; then | |
| echo "- submodule compare: n/a" | |
| else | |
| echo "- submodule compare: ${compare_link}" | |
| fi | |
| if [[ "${commit_link}" == "n/a" ]]; then | |
| echo "- pushed superproject commit: n/a" | |
| else | |
| echo "- pushed superproject commit: ${commit_link}" | |
| fi | |
| if [[ "${existing_failure_issue_url}" == "n/a" ]]; then | |
| echo "- open failure issue: n/a" | |
| elif [[ "${existing_failure_issue_number}" == "n/a" ]]; then | |
| echo "- open failure issue: [${existing_failure_issue_title}](${existing_failure_issue_url})" | |
| else | |
| echo "- open failure issue: [#${existing_failure_issue_number} ${existing_failure_issue_title}](${existing_failure_issue_url})" | |
| fi | |
| echo "" | |
| echo "**Step outcomes:**" | |
| echo "- failure_issue_gate: \`${FAILURE_ISSUE_GATE_OUTCOME:-n/a}\`" | |
| echo "- update: \`${UPDATE_OUTCOME:-n/a}\`" | |
| echo "- ci_checks: \`${CI_CHECKS_OUTCOME:-n/a}\`" | |
| echo "- e2e_checks: \`${E2E_CHECKS_OUTCOME:-n/a}\`" | |
| echo "- verify_oci: \`${VERIFY_OCI_OUTCOME:-n/a}\`" | |
| echo "- commit_push: \`${COMMIT_PUSH_OUTCOME:-n/a}\`" | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| - name: Create issue on CI failure | |
| if: failure() && (steps.ci_checks.outcome == 'failure' || steps.e2e_checks.outcome == 'failure') && steps.failure_issue_gate.outputs.has_open_failure_issue != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| LATEST_COMMIT: ${{ steps.update.outputs.latest_commit }} | |
| SUBMODULE_REPO_URL: ${{ steps.update.outputs.submodule_repo_url }} | |
| SUBMODULE_BRANCH: ${{ steps.update.outputs.submodule_branch }} | |
| run: | | |
| set -euo pipefail | |
| # Ensure required labels exist (--force is a no-op if label already exists) | |
| gh label create "bug" --description "Something isn't working" --color "d73a4a" --force || true | |
| gh label create "automated" --description "Created by automation" --color "0e8a16" --force || true | |
| run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| commit_short="${LATEST_COMMIT:0:12}" | |
| commit_url="${SUBMODULE_REPO_URL:-}/commit/${LATEST_COMMIT:-unknown}" | |
| branch="${SUBMODULE_BRANCH:-dev}" | |
| body="## Automated submodule update failed CI/e2e checks | |
| The [update-opencode-commit workflow](${run_url}) attempted to update the | |
| opencode submodule to [\`${commit_short}\`](${commit_url}) from the | |
| \`${branch}\` branch, but checks failed after auto-formatting. | |
| ### What to do | |
| 1. Check the [workflow run logs](${run_url}) for the specific failure | |
| 2. Common causes: | |
| - **TypeScript typecheck failure** in a fork-* package — fix in the opencode fork and push to \`dev\` | |
| - **Rust clippy/build failure** in opencode-broker — fix in the fork | |
| - **Formatting issue** that \`just fmt\` couldn't resolve — manual fix needed | |
| - **E2E test failure** — UI regression in the web app, check Playwright report | |
| 3. After fixing, the next scheduled run (every 2 hours) will retry automatically | |
| ### Context | |
| - Submodule commit: [\`${commit_short}\`](${commit_url}) | |
| - Fork branch: \`${branch}\` | |
| - Workflow run: [${GITHUB_RUN_ID}](${run_url})" | |
| gh issue create \ | |
| --title "CI checks failed for opencode submodule update (${commit_short})" \ | |
| --label "bug,automated" \ | |
| --body "${body}" |