From 3d9a567009884e8f383cf707e1d071cd8c161e84 Mon Sep 17 00:00:00 2001 From: Hanzo AI Date: Sat, 25 Apr 2026 15:35:02 -0700 Subject: [PATCH 1/3] infra: standardize ZAP duplex port to 9999 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ZAP binary protocol duplex RPC standardizes on :9999 across every service. Lux validator staking/RPC ports (9630/9631 mainnet, 9650/9651 devnet) are unchanged — those are luxd network protocol ports, not ZAP. One ZAP port everywhere (was a mix of 9651/9652/9653/9633/9900). --- hanzo-dev/core/src/model_provider_info.rs | 2 +- hanzo-dev/core/src/zap_client.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hanzo-dev/core/src/model_provider_info.rs b/hanzo-dev/core/src/model_provider_info.rs index 57aab45828a5..08215f711e21 100644 --- a/hanzo-dev/core/src/model_provider_info.rs +++ b/hanzo-dev/core/src/model_provider_info.rs @@ -46,7 +46,7 @@ pub enum WireApi { Chat, /// Native ZAP binary transport over TLS 1.3+PQ. - /// Connects to zap.hanzo.ai:9651 (or ZAP_ENDPOINT env) and sends + /// Connects to zap.hanzo.ai:9999 (or ZAP_ENDPOINT env) and sends /// chat completions as MsgType 100 (native cloud service) messages. Zap, } diff --git a/hanzo-dev/core/src/zap_client.rs b/hanzo-dev/core/src/zap_client.rs index 2e0e6fb13fb1..b2d82ce791de 100644 --- a/hanzo-dev/core/src/zap_client.rs +++ b/hanzo-dev/core/src/zap_client.rs @@ -1,6 +1,6 @@ //! ZAP cloud service client for the dev CLI. //! -//! Connects to `zap.hanzo.ai:9651` via TLS 1.3, performs the luxfi/zap +//! Connects to `zap.hanzo.ai:9999` via TLS 1.3, performs the luxfi/zap //! handshake, and sends MsgType 100 cloud service requests (chat.completions). //! //! This is the real native ZAP binary transport — no HTTP, no fallback. @@ -23,7 +23,7 @@ use tokio::sync::mpsc; use tracing::debug; use tracing::warn; -const DEFAULT_ZAP_ENDPOINT: &str = "zap.hanzo.ai:9651"; +const DEFAULT_ZAP_ENDPOINT: &str = "zap.hanzo.ai:9999"; const CLIENT_NODE_ID: &str = "dev-client"; // ── OpenAI-compatible response types ──────────────────────────────────── From 6fe8dc6c42555de36216361158ef25c4efd24523 Mon Sep 17 00:00:00 2001 From: Hanzo AI Date: Sat, 25 Apr 2026 21:15:45 -0700 Subject: [PATCH 2/3] =?UTF-8?q?ci:=20upstream-sync=20workflow=20=E2=80=94?= =?UTF-8?q?=20auto-merge=20clean,=20issue=20on=20conflict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Periodic sync from just-every/code (the canonical upstream Hanzo dev tracks). Every 6h: fetches upstream, attempts merge, opens PR with automerge label if clean, otherwise opens an issue listing conflicting paths. Bot does not resolve content conflicts. --- .github/workflows/upstream-sync.yml | 126 ++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .github/workflows/upstream-sync.yml diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml new file mode 100644 index 000000000000..6bfdd0d13b64 --- /dev/null +++ b/.github/workflows/upstream-sync.yml @@ -0,0 +1,126 @@ +# Periodic upstream sync. Fetches `just-every/code` (the canonical fork base +# Hanzo dev tracks), attempts a merge into `merge-upstream-updates`, opens +# a PR for review on conflict, auto-merges on clean. +# +# Hanzo overlay paths are kept distinct from upstream so most syncs are clean. +# Conflicting paths are flagged for manual resolution; the bot does NOT +# resolve content conflicts — those need eyes. +# +# Trigger: every 6 hours on schedule, plus manual via workflow_dispatch. + +name: upstream-sync + +on: + schedule: + - cron: '0 */6 * * *' + workflow_dispatch: + inputs: + upstream_remote: + description: 'Upstream remote URL' + required: false + default: 'https://github.com/just-every/code.git' + upstream_branch: + description: 'Upstream branch to sync from' + required: false + default: 'main' + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: checkout main + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: configure git + run: | + git config user.name 'hanzo-upstream-sync[bot]' + git config user.email 'hanzo-upstream-sync@users.noreply.github.com' + + - name: fetch upstream + env: + UPSTREAM_URL: ${{ github.event.inputs.upstream_remote || 'https://github.com/just-every/code.git' }} + UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + run: | + git remote add upstream "$UPSTREAM_URL" + git fetch upstream "$UPSTREAM_BRANCH" --tags --no-recurse-submodules + + - name: short-circuit if already in sync + id: sync_check + env: + UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + run: | + BEHIND=$(git rev-list --count "main..upstream/$UPSTREAM_BRANCH") + echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" + if [ "$BEHIND" = "0" ]; then + echo "Already at upstream HEAD; nothing to do." + fi + + - name: prepare merge branch + if: steps.sync_check.outputs.behind != '0' + env: + UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + run: | + BRANCH="merge-upstream-$(date -u +%Y%m%d-%H%M%S)" + echo "MERGE_BRANCH=$BRANCH" >> "$GITHUB_ENV" + git checkout -b "$BRANCH" main + + - name: attempt merge + id: merge + if: steps.sync_check.outputs.behind != '0' + env: + UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + run: | + set +e + git merge --no-edit "upstream/$UPSTREAM_BRANCH" -m "merge upstream/$UPSTREAM_BRANCH" + STATUS=$? + if [ "$STATUS" -ne 0 ]; then + CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ' ') + git merge --abort + git checkout main + echo "status=conflict" >> "$GITHUB_OUTPUT" + echo "conflicts=$CONFLICTS" >> "$GITHUB_OUTPUT" + else + echo "status=clean" >> "$GITHUB_OUTPUT" + fi + exit 0 + + - name: push merge branch (clean) + if: steps.merge.outputs.status == 'clean' + run: | + git push origin "$MERGE_BRANCH" + + - name: open PR (clean — auto-merge eligible) + if: steps.merge.outputs.status == 'clean' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ env.MERGE_BRANCH }} + base: main + title: 'sync: upstream just-every/code (clean)' + body: | + Automated upstream sync. Merge produced no conflicts. + + Behind by ${{ steps.sync_check.outputs.behind }} commits before this PR. + + Auto-merge will land this once required checks pass. + labels: upstream-sync, automerge + + - name: open PR (conflict — needs human) + if: steps.merge.outputs.status == 'conflict' + run: | + # Push only the marker branch (no commits beyond main); the PR body documents + # conflicting paths so a human can drive the resolution locally. + git push origin "$MERGE_BRANCH" || true + gh issue create \ + --title "upstream-sync: conflict on $(date -u +%Y-%m-%d)" \ + --label upstream-sync,conflict \ + --body "Automated upstream sync hit conflicts. Behind by ${{ steps.sync_check.outputs.behind }} commits. Conflicting paths:\n\n\`\`\`\n${{ steps.merge.outputs.conflicts }}\n\`\`\`\n\nResolve locally:\n\`\`\`\ngit fetch origin\ngit checkout main\ngit merge upstream/${{ github.event.inputs.upstream_branch || 'main' }}\n# resolve conflicts\ngit push\n\`\`\`" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From aca013b8ffcc276c049d531362706af2a5406ddf Mon Sep 17 00:00:00 2001 From: Hanzo AI Date: Sun, 26 Apr 2026 00:33:49 -0700 Subject: [PATCH 3/3] ci: upstream-sync drafts only, one workflow (drop upstream-merge) - delete upstream-merge.yml (1106 lines, auto-merge, polled every 30 min) in favour of single upstream-sync workflow - upstream points to openai/codex (matches LLM.md), not just-every/code - weekly cron Mon 06:00 UTC (was every 6h) - branch named upstream-sync/ - always opens draft PR, never auto-merges - on conflict, commits markers in place and opens draft PR labelled conflict so the branch is checkoutable for human resolution - LLM.md documents the new flow One way: there is now exactly one upstream-sync entry point. --- .github/workflows/upstream-merge.yml | 1106 -------------------------- .github/workflows/upstream-sync.yml | 171 ++-- LLM.md | 29 +- 3 files changed, 124 insertions(+), 1182 deletions(-) delete mode 100644 .github/workflows/upstream-merge.yml diff --git a/.github/workflows/upstream-merge.yml b/.github/workflows/upstream-merge.yml deleted file mode 100644 index dfb7271e906a..000000000000 --- a/.github/workflows/upstream-merge.yml +++ /dev/null @@ -1,1106 +0,0 @@ -name: Upstream Merge - -on: - # Poll upstream regularly; also allow manual and external triggers - schedule: - - cron: "*/30 * * * *" - workflow_dispatch: - inputs: - upstream_repo: - description: "Upstream repo (owner/name)" - required: false - default: "openai/codex" - upstream_branch: - description: "Upstream branch" - required: false - default: "main" - repository_dispatch: - types: [upstream-push] - -concurrency: - group: upstream-merge - # Do not cancel in‑flight runs; upstream merges can be long‑running. Starting - # a new run should not kill the previous one. - cancel-in-progress: false - -permissions: - contents: write - pull-requests: write - -env: - UPSTREAM_REPO: ${{ inputs.upstream_repo || 'openai/codex' }} - UPSTREAM_BRANCH: ${{ inputs.upstream_branch || 'main' }} - MERGE_BRANCH: upstream-merge - # Controls whether we auto-close an existing upstream-merge PR when the run - # skips due to no upstream changes and the branch is zero‑diff vs base. - # This used to be true when we reset the branch each run; now we carry - # forward the branch for incremental merges, so default this to false. - CLOSE_ZERO_DIFF_ON_SKIP: "false" - RG_VERSION: "14.1.0" - JQ_VERSION: "1.7.1" - -jobs: - precheck: - name: Precheck (no-op gate) - runs-on: ubuntu-latest - timeout-minutes: 10 - outputs: - skip_due_to_active: ${{ steps.active_guard.outputs.skip_due_to_active }} - action: ${{ steps.check.outputs.action }} - upstream_only: ${{ steps.check.outputs.upstream_only }} - merge_only: ${{ steps.check.outputs.merge_only }} - upstream_in_merge: ${{ steps.check.outputs.upstream_in_merge }} - steps: - - name: Guard concurrent upstream-merge run - id: active_guard - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.CODE_GH_PAT || github.token }} - script: | - const { owner, repo } = context.repo; - const workflowId = 'upstream-merge.yml'; - const resp = await github.rest.actions.listWorkflowRuns({ - owner, - repo, - workflow_id: workflowId, - per_page: 20 - }); - const blocking = resp.data.workflow_runs.filter(run => - run.status === 'in_progress' && run.id !== context.runId - ); - const skip = blocking.length > 0; - core.setOutput('skip_due_to_active', skip ? 'true' : 'false'); - - if (skip) { - const latest = blocking[0]; - await core.summary - .addHeading('Upstream Merge: Active Run Guard') - .addRaw(`- blocking_runs: ${blocking.length}`, true) - .addRaw(`- blocking_run: #${latest.run_number} (${latest.status})`, true) - .addLink('Blocking run logs', latest.html_url) - .write(); - core.notice(`Another Upstream Merge run (#${latest.run_number}) is still ${latest.status}; skipping remaining steps.`); - core.info('Exiting early because another upstream-merge run is still active.'); - } - - - name: Check out repository (full history) - if: steps.active_guard.outputs.skip_due_to_active != 'true' - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - - name: "Quick precheck: compute upstream/merge deltas" - if: steps.active_guard.outputs.skip_due_to_active != 'true' - id: check - shell: bash - env: - UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }} - UPSTREAM_BRANCH: ${{ env.UPSTREAM_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - run: | - set -euo pipefail - git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null || true - git fetch --no-tags --prune origin "${DEFAULT_BRANCH}" - HAS_MERGE_BRANCH=false - if git ls-remote --exit-code --heads origin "${MERGE_BRANCH}" >/dev/null 2>&1; then - git fetch --no-tags --prune origin "${MERGE_BRANCH}" - HAS_MERGE_BRANCH=true - fi - git fetch --no-tags --prune upstream \ - "+refs/heads/${UPSTREAM_BRANCH}:refs/remotes/upstream/${UPSTREAM_BRANCH}" - # Count exclusive commits to determine work type - upstream_only=$(git rev-list --count "upstream/${UPSTREAM_BRANCH}" --not "origin/${DEFAULT_BRANCH}" || echo 0) - merge_only=0 - if [ "$HAS_MERGE_BRANCH" = true ]; then - merge_only=$(git rev-list --count "origin/${MERGE_BRANCH}" --not "origin/${DEFAULT_BRANCH}" || echo 0) - fi - upstream_in_merge=false - if [ "$HAS_MERGE_BRANCH" = true ] && git merge-base --is-ancestor "upstream/${UPSTREAM_BRANCH}" "origin/${MERGE_BRANCH}"; then - upstream_in_merge=true - fi - - action=no_work - if [ "$upstream_only" -gt 0 ] && [ "$upstream_in_merge" != true ]; then - action=merge - elif [ "$merge_only" -gt 0 ] || [ "$upstream_in_merge" = true ]; then - action=pr_only - fi - - { - echo "action=$action"; - echo "upstream_only=$upstream_only"; - echo "merge_only=$merge_only"; - echo "upstream_in_merge=$upstream_in_merge"; - } | tee -a "$GITHUB_OUTPUT" - - { - echo "### Upstream Merge: Precheck"; - echo "- action: ${action}"; - echo "- upstream_only: ${upstream_only}"; - echo "- merge_only: ${merge_only}"; - echo "- upstream_in_merge: ${upstream_in_merge}"; - } >> "$GITHUB_STEP_SUMMARY" - - - name: Close stale upstream-merge PR on skip when zero diff (opt-in) - if: steps.active_guard.outputs.skip_due_to_active != 'true' && steps.check.outputs.skip == 'true' && steps.check.outputs.mirror_on_skip == 'true' && env.CLOSE_ZERO_DIFF_ON_SKIP == 'true' - uses: actions/github-script@v7 - env: - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - with: - github-token: ${{ secrets.CODE_GH_PAT || github.token }} - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const head = process.env.MERGE_BRANCH; - const base = process.env.DEFAULT_BRANCH; - const headRef = `${owner}:${head}`; - const prs = await github.rest.pulls.list({ owner, repo, state: 'open', head: headRef }); - if (!prs.data.length) { return; } - let zeroDiff = false; - try { - const cmp = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${base}...${head}` }); - zeroDiff = (cmp.data.files || []).length === 0; - } catch (e) { - core.warning(`Compare failed (${base}...${head}): ${e.message}. Skipping.`); - return; - } - if (zeroDiff) { - const pr = prs.data[0]; - await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: 'Closing: upstream merge has no net file changes vs base.' }); - await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' }); - core.notice(`Closed PR #${pr.number} due to zero diff (skip path).`); - } - - merge: - needs: [precheck] - if: needs.precheck.outputs.skip_due_to_active != 'true' && needs.precheck.outputs.action == 'merge' - runs-on: ubuntu-latest - timeout-minutes: 45 - - steps: - - name: Check out repository (full history) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Configure authenticated origin for pushes - env: - GH_TOKEN: ${{ secrets.CODE_GH_PAT || github.token }} - REPO: ${{ github.repository }} - run: | - set -euo pipefail - git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" - - - name: Set git identity for commits - run: | - git config user.name "hanzoai-dev" - git config user.email "dev@hanzo.ai" - git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: "Quick no-op: ancestor check (default and merge branch)" - id: check_upstream - shell: bash - env: - UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }} - UPSTREAM_BRANCH: ${{ env.UPSTREAM_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - run: | - set -euo pipefail - git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null || true - # Fetch exact refs with commit graph, no blobs, to make ancestor checks reliable and fast. - # NOTE: Do not consult origin/upstream-merge here — it may contain a prior partial merge and - # cause false positives. We primarily compare upstream vs the default branch, but if a dedicated - # merge branch exists we also consider it for no-op (so we don't re-trigger while a PR is open). - git fetch --no-tags --prune --filter=blob:none origin "${DEFAULT_BRANCH}" - HAS_MERGE_BRANCH=false - if git ls-remote --exit-code --heads origin "${MERGE_BRANCH}" >/dev/null 2>&1; then - git fetch --no-tags --prune --filter=blob:none origin "${MERGE_BRANCH}" - HAS_MERGE_BRANCH=true - fi - git fetch --no-tags --prune --filter=blob:none upstream \ - "+refs/heads/${UPSTREAM_BRANCH}:refs/remotes/upstream/${UPSTREAM_BRANCH}" - # Evaluate ancestor relationships and expose results as step outputs - if git merge-base --is-ancestor "upstream/${UPSTREAM_BRANCH}" "origin/${DEFAULT_BRANCH}"; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "skip_reason=upstream_ancestor_of_default" >> "$GITHUB_OUTPUT" - echo "mirror_on_skip=true" >> "$GITHUB_OUTPUT" - elif [ "$HAS_MERGE_BRANCH" = true ] && git merge-base --is-ancestor "upstream/${UPSTREAM_BRANCH}" "origin/${MERGE_BRANCH}"; then - echo "skip=true" >> "$GITHUB_OUTPUT" - echo "skip_reason=upstream_ancestor_of_merge_branch" >> "$GITHUB_OUTPUT" - echo "mirror_on_skip=false" >> "$GITHUB_OUTPUT" - else - echo "skip=false" >> "$GITHUB_OUTPUT" - echo "skip_reason=upstream_has_new_commits" >> "$GITHUB_OUTPUT" - echo "mirror_on_skip=false" >> "$GITHUB_OUTPUT" - fi - - - name: Summarize quick check - if: always() - run: | - { - echo "### Upstream Merge: Quick Check"; - echo "- skip: ${{ steps.check_upstream.outputs.skip || '' }}"; - echo "- reason: ${{ steps.check_upstream.outputs.skip_reason || 'n/a' }}"; - echo "- mirror_on_skip: ${{ steps.check_upstream.outputs.mirror_on_skip || 'false' }}"; - } >> "$GITHUB_STEP_SUMMARY" - - # Expensive setup only runs if we are not skipping. - - name: Setup Rust toolchain (match repo) - if: steps.check_upstream.outputs.skip != 'true' - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.90.0 - - - name: Add local bin to PATH - if: steps.check_upstream.outputs.skip != 'true' - run: | - mkdir -p "$HOME/.local/bin" - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - - - name: Set shared Cargo env (CARGO_HOME, CARGO_TARGET_DIR) - if: steps.check_upstream.outputs.skip != 'true' - run: | - echo "CARGO_HOME=${RUNNER_TEMP}/cargo-home" >> "$GITHUB_ENV" - echo "CARGO_TARGET_DIR=${GITHUB_WORKSPACE}/hanzo-dev/target" >> "$GITHUB_ENV" - - - name: Cache ripgrep binary - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/cache@v4 - with: - path: ~/.local/tools/rg-${{ env.RG_VERSION }} - key: rg-${{ runner.os }}-${{ env.RG_VERSION }} - - - name: Setup ripgrep (cached) - if: steps.check_upstream.outputs.skip != 'true' - run: | - set -euo pipefail - if command -v rg >/dev/null 2>&1; then rg --version; exit 0; fi - mkdir -p "$HOME/.local/tools" "$HOME/.local/bin" - if [ ! -x "$HOME/.local/tools/rg-${RG_VERSION}/rg" ]; then - cd "$HOME/.local/tools" - TARBALL="ripgrep-${RG_VERSION}-x86_64-unknown-linux-musl.tar.gz" - URL="https://github.com/BurntSushi/ripgrep/releases/download/${RG_VERSION}/${TARBALL}" - curl -sSL "$URL" -o "$TARBALL" - tar -xzf "$TARBALL" - rm -f "$TARBALL" - mv "ripgrep-${RG_VERSION}-x86_64-unknown-linux-musl" "rg-${RG_VERSION}" - fi - install -m 0755 "$HOME/.local/tools/rg-${RG_VERSION}/rg" "$HOME/.local/bin/rg" - rg --version - - - name: Cache jq binary - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/cache@v4 - with: - path: ~/.local/tools/jq-${{ env.JQ_VERSION }} - key: jq-${{ runner.os }}-${{ env.JQ_VERSION }} - - - name: Setup jq (cached) - if: steps.check_upstream.outputs.skip != 'true' - run: | - set -euo pipefail - if command -v jq >/dev/null 2>&1; then jq --version; exit 0; fi - mkdir -p "$HOME/.local/tools/jq-${JQ_VERSION}" "$HOME/.local/bin" - URL="https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64" - curl -sSL "$URL" -o "$HOME/.local/tools/jq-${JQ_VERSION}/jq" - chmod +x "$HOME/.local/tools/jq-${JQ_VERSION}/jq" - install -m 0755 "$HOME/.local/tools/jq-${JQ_VERSION}/jq" "$HOME/.local/bin/jq" - jq --version - - # Remove slow apt install; we now use cached static binaries for rg/jq - - - name: Cache Rust build (cargo + target) - if: steps.check_upstream.outputs.skip != 'true' - uses: Swatinem/rust-cache@v2 - with: - # Our Rust workspace lives in codex-rs; cache its target dir - workspaces: | - codex-rs -> target - save-if: true - cache-on-failure: true - - - name: Setup sccache (GHA backend) - if: steps.check_upstream.outputs.skip != 'true' - uses: mozilla-actions/sccache-action@v0.0.9 - with: - version: v0.10.0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Enable sccache - if: steps.check_upstream.outputs.skip != 'true' - run: | - echo 'SCCACHE_GHA_ENABLED=true' >> "$GITHUB_ENV" - echo 'RUSTC_WRAPPER=sccache' >> "$GITHUB_ENV" - echo 'SCCACHE_IDLE_TIMEOUT=1800' >> "$GITHUB_ENV" - echo 'SCCACHE_CACHE_SIZE=5G' >> "$GITHUB_ENV" - - # Remove redundant direct caches; Swatinem/rust-cache covers target and cargo dirs via CARGO_HOME - - - name: Prime Rust build cache (fast local build) - if: steps.check_upstream.outputs.skip != 'true' - env: - STRICT_CARGO_HOME: "1" - CARGO_HOME_ENFORCED: ${{ env.CARGO_HOME }} - run: | - set -euo pipefail - ./build-fast.sh - - - name: Setup Node.js - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Cache npm (npx) downloads - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/cache@v4 - with: - path: | - ~/.npm - key: npm-cache-${{ runner.os }}-node20-${{ hashFiles('**/package-lock.json', '**/pnpm-lock.yaml', '**/yarn.lock') }} - restore-keys: | - npm-cache-${{ runner.os }}-node20- - - - name: Start local OpenAI proxy (no key to agent; verbose logging) - if: steps.check_upstream.outputs.skip != 'true' - id: openai_proxy - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: | - set -euo pipefail - if [ -z "${OPENAI_API_KEY:-}" ]; then - echo "OPENAI_API_KEY secret is required to start the proxy." >&2 - exit 1 - fi - mkdir -p .github/auto - PORT=5055 LOG_DEST=stdout EXIT_ON_5XX=1 RESPONSES_BETA="responses=v1" node scripts/openai-proxy.js > .github/auto/openai-proxy.log 2>&1 & - echo "pid=$!" >> "$GITHUB_OUTPUT" - # Wait briefly for readiness - for i in {1..30}; do if nc -z 127.0.0.1 5055; then break; else sleep 0.2; fi; done || true - - - name: Print proxy startup log tail - if: steps.check_upstream.outputs.skip != 'true' - run: | - set -euo pipefail - echo '### openai-proxy.log (tail)' >> "$GITHUB_STEP_SUMMARY" - { echo '```'; tail -n 80 .github/auto/openai-proxy.log || true; echo '```'; } >> "$GITHUB_STEP_SUMMARY" - - - name: Prepare agent context (commit range, deleted paths, histogram) - id: prep - if: steps.check_upstream.outputs.skip != 'true' - env: - UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }} - UPSTREAM_BRANCH: ${{ env.UPSTREAM_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - run: | - set -euo pipefail - set -x - mkdir -p .github/auto - git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null || true - # Fetch with commit graph but no blobs for speed; ensure sufficient history for merge-base - git fetch --no-tags origin --prune --filter=blob:none - git fetch --no-tags upstream --prune --filter=blob:none "${UPSTREAM_BRANCH}" - RANGE="upstream/${UPSTREAM_BRANCH} ^origin/${DEFAULT_BRANCH}" - : > .github/auto/COMMITS.json - echo '[' >> .github/auto/COMMITS.json - first=1 - while read -r sha; do - title=$(git log -1 --pretty=%s "$sha" | sed 's/"/\\"/g') - date=$(git log -1 --pretty=%cI "$sha") - files=$(git show --pretty=format: --name-only "$sha" | sed '/^$/d') - files_json=$(printf '%s\n' "$files" | jq -Rcs 'split("\n") | map(select(length>0))') - stats=$(git show --shortstat --oneline "$sha" | tail -n1) - ins=$(printf '%s' "$stats" | sed -n 's/.* \([0-9]\+\) insertions\?.*/\1/p') - del=$(printf '%s' "$stats" | sed -n 's/.* \([0-9]\+\) deletions\?.*/\1/p') - ins=${ins:-0}; del=${del:-0} - [ $first -eq 1 ] || echo ',' >> .github/auto/COMMITS.json - first=0 - jq -n --arg sha "$sha" --arg title "$title" --arg date "$date" \ - --argjson files "$files_json" --argjson insertions "$ins" --argjson deletions "$del" \ - '{sha:$sha,title:$title,date:$date,files:$files,insertions:($insertions|tonumber),deletions:($deletions|tonumber)}' >> .github/auto/COMMITS.json - done < <(git rev-list --reverse $RANGE) - echo ']' >> .github/auto/COMMITS.json - if jq -e 'length==0' .github/auto/COMMITS.json >/dev/null 2>&1; then - echo "No upstream commits beyond default; context prepared (empty)."; - fi - - git ls-tree -r --name-only "origin/${DEFAULT_BRANCH}" | awk -F'/' '/^codex-rs\//{print $1"/"$2}' | sort -u > .github/auto/DEFAULT_CRATES.txt - git ls-tree -r --name-only "upstream/${UPSTREAM_BRANCH}" | awk -F'/' '/^codex-rs\//{print $1"/"$2}' | sort -u > .github/auto/UPSTREAM_CRATES.txt - comm -13 .github/auto/DEFAULT_CRATES.txt .github/auto/UPSTREAM_CRATES.txt > .github/auto/DELETED_ON_DEFAULT.txt || true - - git diff --name-only "origin/${DEFAULT_BRANCH}..upstream/${UPSTREAM_BRANCH}" > .github/auto/DELTA_FILES.txt || true - awk 'BEGIN{tui=cli=core=docs=tests=other=0} - /^codex-rs\/tui\//{tui++; next} - /^codex-cli\//{cli++; next} - /^codex-rs\/(core|common|protocol|exec|file-search)\//{core++; next} - /^docs\//{docs++; next} - /(^|\/)tests?\//{tests++; next} - {other++} - END{printf("tui=%d cli=%d core=%d docs=%d tests=%d other=%d\n",tui,cli,core,docs,tests,other)}' \ - .github/auto/DELTA_FILES.txt > .github/auto/CHANGE_HISTOGRAM.txt - - FILES_COUNT=$(wc -l < .github/auto/DELTA_FILES.txt | tr -d ' ') - LOC_EST=$(git diff --shortstat "origin/${DEFAULT_BRANCH}..upstream/${UPSTREAM_BRANCH}" | awk '{for(i=1;i<=NF;i++){if($i=="insertions(+)")ins=$(i-1); if($i=="deletions(-)")del=$(i-1)} } END{print (ins?ins:0)+(del?del:0)}') - MERGE_MODE=one-shot - if [ "${FILES_COUNT:-0}" -gt 800 ] || [ "${LOC_EST:-0}" -gt 15000 ]; then MERGE_MODE=by-bucket; fi - echo "merge_mode=${MERGE_MODE}" >> "$GITHUB_OUTPUT" - echo "files_count=${FILES_COUNT}" >> "$GITHUB_OUTPUT" - echo "loc_est=${LOC_EST}" >> "$GITHUB_OUTPUT" - - git diff --stat "origin/${DEFAULT_BRANCH}..upstream/${UPSTREAM_BRANCH}" > .github/auto/DIFFSTAT.txt || true - - # Detect reintroduced paths: present in merge-base, absent on default, present on upstream - MB=$(git merge-base "origin/${DEFAULT_BRANCH}" "upstream/${UPSTREAM_BRANCH}" 2>/dev/null || true) - : > .github/auto/REINTRODUCED_PATHS.txt - if [ -n "${MB:-}" ]; then - while read -r status path; do - [ "${status}" = "A" ] || continue - if git ls-tree -r --name-only "$MB" -- "$path" >/dev/null 2>&1 && \ - git ls-tree -r --name-only "$MB" -- "$path" | grep -q . && \ - ! git ls-tree -r --name-only "origin/${DEFAULT_BRANCH}" -- "$path" | grep -q .; then - echo "$path" >> .github/auto/REINTRODUCED_PATHS.txt - fi - done < <(git diff --name-status "origin/${DEFAULT_BRANCH}..upstream/${UPSTREAM_BRANCH}" || true) - else - echo "No merge-base between origin/${DEFAULT_BRANCH} and upstream/${UPSTREAM_BRANCH}; skipping reintroduced path detection." >> .github/auto/CHANGE_HISTOGRAM.txt - fi - if [ -z "${MB:-}" ]; then echo "no_merge_base=true" >> "$GITHUB_OUTPUT"; else echo "no_merge_base=false" >> "$GITHUB_OUTPUT"; fi - - - name: Carry forward merge branch (merge default into it) - if: steps.check_upstream.outputs.skip != 'true' - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - run: | - set -euo pipefail - # Bring local refs up to date - git fetch --no-tags --prune --filter=blob:none origin - # If upstream-merge exists, base work on it and merge the latest default into it, - # preferring existing upstream-merge resolutions on conflict (-X ours). - if git ls-remote --exit-code --heads origin upstream-merge >/dev/null 2>&1; then - git checkout -B upstream-merge origin/upstream-merge - # Merge the latest default into upstream-merge to rebase the carry-forward state - git merge --no-ff --no-edit -X ours "origin/${DEFAULT_BRANCH}" || true - # Do not push yet; wait for the agent's merge so we do not clobber prior upstream commits on failure. - else - # First run: create the branch from the current default; agent step will publish it after a successful merge. - git checkout -B upstream-merge "origin/${DEFAULT_BRANCH}" - fi - - - name: Close accidental PRs targeting upstream-merge (we mirror default) - if: always() - uses: actions/github-script@v7 - env: - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - with: - github-token: ${{ secrets.CODE_GH_PAT || github.token }} - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const base = process.env.MERGE_BRANCH; // upstream-merge - // Close any open PR that targets upstream-merge; we mirror default directly. - const prs = await github.rest.pulls.list({ owner, repo, state: 'open', base }); - for (const pr of prs.data) { - await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: 'Closing: this repo auto-mirrors the default branch into upstream-merge. Please target `main` instead.' }); - await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' }); - core.notice(`Closed PR #${pr.number} targeting ${base}.`); - } - - - name: Run Code agent to perform upstream merge - id: agent - if: steps.check_upstream.outputs.skip != 'true' - env: - UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }} - UPSTREAM_BRANCH: ${{ env.UPSTREAM_BRANCH }} - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - GH_TOKEN: ${{ secrets.CODE_GH_PAT || github.token }} - MERGE_MODE: ${{ steps.prep.outputs.merge_mode || 'one-shot' }} - OURS_GLOBS: | - codex-rs/tui/** - codex-cli/** - .github/workflows/** - AGENTS.md - README.md - CHANGELOG.md - run: | - set -euo pipefail - SAFE_PATH="$PATH"; SAFE_HOME="$HOME" - # Build the agent prompt safely without command substitution - { - printf 'Context\n- UPSTREAM_REPO=%s\n- UPSTREAM_BRANCH=%s\n- MERGE_BRANCH=%s\n- DEFAULT_BRANCH=%s\n\n' \ - "$UPSTREAM_REPO" "$UPSTREAM_BRANCH" "$MERGE_BRANCH" "$DEFAULT_BRANCH"; - echo 'Runtime'; echo '- ENV: github-actions'; echo "- MERGE_MODE=${MERGE_MODE}"; echo ''; - echo 'Goals'; - echo '- Keep our fork in sync with upstream by incorporating genuine improvements.'; - echo '- Do not overwrite our unique TUI and tooling approach unless clearly beneficial and compatible.'; - echo '- Make granular decisions commit-by-commit or by bucket; do not blanket-drop upstream changes without review.'; - echo '- Preserve our added functionality in core: model-driven browser tools, agent tools, screenshot handling, and version/UA semantics.'; - echo ''; - echo 'Fork Enhancements (initial, not exhaustive)'; - if [ -f docs/fork-enhancements.md ]; then - sed -n '1,200p' docs/fork-enhancements.md; - else - echo '- (fork overview file missing)'; - fi - echo ''; - echo 'Artifacts'; - echo '- .github/auto/COMMITS.json: upstream commits not in default (sha, title, files, stats).'; - echo '- .github/auto/DELETED_ON_DEFAULT.txt: crates/paths removed on our default; avoid re-introducing.'; - echo '- .github/auto/CHANGE_HISTOGRAM.txt: rough areas touched.'; - echo '- .github/auto/DELTA_FILES.txt and DIFFSTAT.txt: filenames and summary.'; - echo '- .github/auto/REINTRODUCED_PATHS.txt: candidate paths removed previously that upstream reintroduced.'; - echo ''; - # Emit a minimized JSON policy for clarity (drop empty sections) - if command -v jq >/dev/null 2>&1; then - jq 'del(.prefer_theirs_globs) | with_entries(select(.value|type != "array" or (.value|length>0)))' .github/merge-policy.json 2>/dev/null || cat .github/merge-policy.json || echo '{ }' - else - cat .github/merge-policy.json 2>/dev/null || echo '{ }' - fi - echo ''; - cat << 'EOP' - - You are the maintainer bot. Perform an upstream merge using our repo policies and a selective reconciliation strategy. - Steps: - 1) Ensure remote `upstream` points to the UPSTREAM_REPO in Context. Fetch origin and upstream. - 2) Write .github/auto/MERGE_PLAN.md summarizing strategy (one-shot, by-bucket, or per-commit) based on MERGE_MODE and artifacts. - 3) Use existing MERGE_BRANCH (prepared earlier). Do not reset or recreate it. - 4) Merge upstream/UPSTREAM_BRANCH into MERGE_BRANCH using `--no-commit`. - - Use the JSON for prefer_ours_globs, prefer_theirs_globs, and purge_globs. - - Default: adopt upstream outside prefer_ours_globs. In protected areas (prefer_ours_globs), keep ours unless you identify a clearly beneficial, compatible upstream change. - - For files matching prefer_theirs_globs, lean towards upstream unless it breaks our build or documented behavior. - - Explicit invariants to preserve in this fork (must not regress): - • Tool families: any custom handlers with names starting with `browser_` or `agent_`, and `web_fetch` if present, must have corresponding tool schemas exposed by openai_tools (verify.sh enforces handler↔tool parity generically). - • Exposure gating: do not drop the browser gating logic that controls when browser tools are advertised (adapt to upstream refactors without removing the behavior). - • Screenshot UX: do not change screenshot queuing semantics across turns unless you update both producer/consumer paths to preserve UX; prefer preserving our pending queue + TUI updates. - • Version/UA: keep codex_version::version() usage for UA/build and keep get_codex_user_agent_default() for MCP server user agent. - - Do NOT blanket-delete new crates or reintroduced paths. Surface noteworthy cases in MERGE_REPORT.md and make a reasoned choice. - - For any path listed in purge_globs or perma_removed_paths, ensure it remains deleted if upstream reintroduced it. - - Review the upstream commit range (e.g., via `git rev-list upstream/UPSTREAM_BRANCH ^origin/DEFAULT_BRANCH`). Use repo context and the provided artifacts to make sensible, minimal decisions. Prefer preserving our local UX/branding and workflows; adopt upstream when it improves correctness, security, or compatibility. Record notable decisions in MERGE_REPORT.md. - 5) Resolve lockfile conflicts early: if `codex-rs/Cargo.lock` contains merge markers or becomes out of sync with the workspace manifests, regenerate it inside `codex-rs/` (e.g., `cargo update --workspace --locked`; fall back to `cargo update --workspace` if the locked run fails). Commit the regenerated lock as part of the merge and note the action in MERGE_REPORT.md. Prefer preserving our crate versions, but do not leave conflict markers in place. - 6) Compatibility (do not break callers): - - Keep these public re-exports in codex-core: ModelClient, Prompt, ResponseEvent, ResponseStream. - Removing them breaks downstream imports and will fail API tests. - - Keep codex_core::models namespace as an alias to protocol models. - - Do not remove ICU/sys-locale dependencies unless you confirm (via repo-wide search) they are unused across the workspace. - 7) Verify with scripts/upstream-merge/verify.sh. If it fails, fix minimally and re-run until it passes. - - Note: verify.sh includes fork-specific guards for tool registration and UA/version; honor these when resolving conflicts. - 8) Stage and commit with a conventional message and short build status. - 9) Write .github/auto/MERGE_REPORT.md (Incorporated / Dropped / Other changes) summarizing choices. - 10) Push MERGE_BRANCH and prepare PR title/body. - - - - - Be minimal and surgical; do not refactor. - - Keep diffs focused on merge and required fixes. Do not recreate locally removed theming/UX files. - - Never rewrite git history outside the merge branch. - - If Git reports no merge-base between origin/DEFAULT_BRANCH and upstream/UPSTREAM_BRANCH, you may use `--allow-unrelated-histories` to graft histories and proceed with all policies/guards. - - If the initial `git merge --no-ff --no-commit upstream/UPSTREAM_BRANCH` fails with unrelated histories, re-run the merge in a separate command with `--allow-unrelated-histories` (do not chain with `||`). - - Use only the provided GH_TOKEN for push; do not echo it. - - - - To search for API usages before removing exports: - rg -n "^(use\\s+codex_core::|codex_core::)(ModelClient|Prompt|Response(Event|Stream))\\b" codex-rs - - To look for ICU/sys-locale usage across workspace: - rg -n "\\b(sys_locale|icu_(decimal|locale_core))\\b" codex-rs - - To compile API tests without running them: - cargo check -p codex-core --tests --quiet - - EOP - } > .github/auto/AUTO_GOAL.md - - env -i PATH="$SAFE_PATH" HOME="$SAFE_HOME" \ - RUSTC_WRAPPER="sccache" SCCACHE_GHA_ENABLED="true" SCCACHE_IDLE_TIMEOUT="1800" SCCACHE_CACHE_SIZE="5G" \ - OPENAI_API_KEY="x" \ - OPENAI_BASE_URL="http://127.0.0.1:5055/v1" \ - OPENAI_API_BASE="http://127.0.0.1:5055/v1" \ - GH_TOKEN="$GH_TOKEN" \ - npm_config_cache="$SAFE_HOME/.npm" \ - npx -y @hanzo/dev@latest \ - auto \ - --goal-file .github/auto/AUTO_GOAL.md \ - --max-attempts 3 \ - --cd "$GITHUB_WORKSPACE" \ - --skip-git-repo-check \ - -s workspace-write \ - -c sandbox_workspace_write.allow_git_writes=true \ - -c sandbox_workspace_write.network_access=true \ - -c shell_environment_policy.r#set.CARGO_HOME="${RUNNER_TEMP}/cargo-home" \ - -c shell_environment_policy.r#set.CARGO_TARGET_DIR="${GITHUB_WORKSPACE}/codex-rs/target" \ - -c shell_environment_policy.r#set.RUSTC_WRAPPER="sccache" \ - -c shell_environment_policy.r#set.SCCACHE_GHA_ENABLED="true" \ - -c shell_environment_policy.r#set.SCCACHE_IDLE_TIMEOUT="1800" \ - -c shell_environment_policy.r#set.SCCACHE_CACHE_SIZE="5G" \ - -c shell_environment_policy.r#set.KEEP_ENV="1" \ - --json-report .github/auto/AGENT_REPORT.json \ - | tee .github/auto/AGENT_STDOUT.txt - - - name: Assert agent success (fail on streaming/errors) - if: steps.check_upstream.outputs.skip != 'true' - run: | - set -euo pipefail - echo "Checking agent output for fatal errors..." - if rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/AGENT_STDOUT.txt >/dev/null 2>&1; then - echo "Agent reported a fatal error (stream/server). Failing job." >&2 - rg -n "^\\[.*\\] ERROR: (stream error|server error|exceeded retry limit)" .github/auto/AGENT_STDOUT.txt || true - exit 1 - fi - # Also flag 5xx responses from the proxy logs if present - if [ -s .github/auto/openai-proxy.log ]; then - if rg -n '"phase":"response_head".*"status":5\\d\\d' .github/auto/openai-proxy.log >/dev/null 2>&1; then - echo "Proxy observed 5xx from upstream during agent run. Failing job." >&2 - rg -n '"phase":"response_head".*"status":5\\d\\d' .github/auto/openai-proxy.log | tail -n 5 || true - exit 1 - fi - if rg -n '"phase":"upstream_error"' .github/auto/openai-proxy.log >/dev/null 2>&1; then - echo "Proxy upstream_error entries found. Failing job." >&2 - rg -n '"phase":"upstream_error"' .github/auto/openai-proxy.log | tail -n 10 || true - exit 1 - fi - fi - - - name: Upload artifact - openai-proxy.log - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: openai-proxy.log - if-no-files-found: warn - path: .github/auto/openai-proxy.log - - - name: Post-merge verify (build-fast + API compile) - id: verify - if: steps.check_upstream.outputs.skip != 'true' - working-directory: . - run: | - set -euo pipefail - mkdir -p "$GITHUB_WORKSPACE/.github/auto" - bash scripts/upstream-merge/verify.sh || true - cat .github/auto/VERIFY.json || true - if jq -e '.build_fast=="ok" and .api_check=="ok"' .github/auto/VERIFY.json >/dev/null 2>&1; then echo "failed=false" >> "$GITHUB_OUTPUT"; else echo "failed=true" >> "$GITHUB_OUTPUT"; fi - - - name: Remediate verification failures with Code agent (loop) - id: remediation_loop - if: steps.check_upstream.outputs.skip != 'true' && steps.verify.outputs.failed == 'true' - env: - GH_TOKEN: ${{ secrets.CODE_GH_PAT || github.token }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - MAX_REMEDIATION_ATTEMPTS: ${{ env.MAX_REMEDIATION_ATTEMPTS || '3' }} - run: | - set -euo pipefail - SAFE_PATH="$PATH"; SAFE_HOME="$HOME" - : > "$GITHUB_WORKSPACE/.github/auto/AGENT_REMEDIATION_STDOUT.txt" - MAX_ATTEMPTS=${MAX_REMEDIATION_ATTEMPTS:-3} - attempt=1 - - while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do - echo "::notice::Remediation attempt $attempt of $MAX_ATTEMPTS" - { - echo 'Context'; - echo "- MERGE_BRANCH=${MERGE_BRANCH}"; - echo "- DEFAULT_BRANCH=${DEFAULT_BRANCH}"; - echo "- ATTEMPT=${attempt}/${MAX_ATTEMPTS}"; - echo ''; - echo 'Previous verify logs (tail):'; - echo '---8<---'; - tail -n 200 "$GITHUB_WORKSPACE/.github/auto/VERIFY_build-fast.log" 2>/dev/null || true - tail -n 200 "$GITHUB_WORKSPACE/.github/auto/VERIFY_api-check.log" 2>/dev/null || true - tail -n 200 "$GITHUB_WORKSPACE/.github/auto/VERIFY_guards.log" 2>/dev/null || true - echo '---8<---'; - echo ''; - cat << 'PROMPT' - You are acting as a maintainer to remediate compile-only API test failures after an upstream merge. - - Tasks (minimal and surgical): - - Restore or add back missing public re-exports in codex-core that cause `cargo check --tests` to fail (e.g., ModelClient, Prompt, ResponseEvent, ResponseStream), unless you can conclusively update all workspace imports safely. - - Prefer adding compatibility aliases over large refactors. - - Do not remove or rename crates. Do not drop ICU/sys-locale unless unused repo-wide. - - If verification failed due to `codex-rs/Cargo.lock` parse errors or merge markers, regenerate the lock inside `codex-rs/` (try `cargo update --workspace --locked`; fall back to `cargo update --workspace` if necessary) and include the updated file in the fix. - - Run scripts/upstream-merge/verify.sh and iterate until it returns success. - - Update .github/auto/MERGE_REPORT.md with a short note under "Other changes" describing the compatibility adjustments. - - Push to ${MERGE_BRANCH}. - - Constraints: - - Keep changes minimal and focused on making tests compile. - - Do not modify TUI/CLI user-visible branding. - - No history rewriting; only commit on ${MERGE_BRANCH}. - - Treat warnings as failures during the build-fast step. - PROMPT - } | env -i PATH="$SAFE_PATH" HOME="$SAFE_HOME" \ - OPENAI_API_KEY="x" \ - OPENAI_BASE_URL="http://127.0.0.1:5055/v1" \ - OPENAI_API_BASE="http://127.0.0.1:5055/v1" \ - GH_TOKEN="$GH_TOKEN" \ - RUSTC_WRAPPER="sccache" SCCACHE_GHA_ENABLED="true" SCCACHE_IDLE_TIMEOUT="1800" SCCACHE_CACHE_SIZE="5G" \ - npm_config_cache="$SAFE_HOME/.npm" \ - npx -y @hanzo/dev@latest \ - exec \ - -s workspace-write \ - -c sandbox_workspace_write.allow_git_writes=true \ - -c sandbox_workspace_write.network_access=true \ - -c shell_environment_policy.r#set.CARGO_HOME="${RUNNER_TEMP}/cargo-home" \ - -c shell_environment_policy.r#set.CARGO_TARGET_DIR="${GITHUB_WORKSPACE}/codex-rs/target" \ - -c shell_environment_policy.r#set.RUSTC_WRAPPER="sccache" \ - -c shell_environment_policy.r#set.SCCACHE_GHA_ENABLED="true" \ - -c shell_environment_policy.r#set.SCCACHE_IDLE_TIMEOUT="1800" \ - -c shell_environment_policy.r#set.SCCACHE_CACHE_SIZE="5G" \ - -c shell_environment_policy.r#set.KEEP_ENV="1" \ - --cd "$GITHUB_WORKSPACE" \ - --skip-git-repo-check \ - - | tee -a "$GITHUB_WORKSPACE/.github/auto/AGENT_REMEDIATION_STDOUT.txt" - - if bash scripts/upstream-merge/verify.sh; then - echo "remediated=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - attempt=$((attempt + 1)) - if [ "$attempt" -gt "$MAX_ATTEMPTS" ]; then - echo "::error::Verification still failing after ${MAX_ATTEMPTS} remediation attempts" >&2 - exit 1 - fi - done - - - name: Upload artifact - VERIFY logs - if: steps.check_upstream.outputs.skip != 'true' && always() - uses: actions/upload-artifact@v4 - with: - name: VERIFY - if-no-files-found: warn - path: | - .github/auto/VERIFY.json - .github/auto/VERIFY_build-fast.log - .github/auto/VERIFY_api-check.log - .github/auto/VERIFY_guards.log - .github/auto/AGENT_REMEDIATION_STDOUT.txt - - - name: Enforce policy removals on merge branch (images + caches + purge_globs) - if: steps.check_upstream.outputs.skip != 'true' - env: - GH_TOKEN: ${{ secrets.CODE_GH_PAT || github.token }} - REPO: ${{ github.repository }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - run: | - set -euo pipefail - OWNER=${REPO%%/*} - REPO_N=${REPO##*/} - # Only proceed if branch exists - if ! git ls-remote --exit-code --heads origin upstream-merge >/dev/null 2>&1; then - echo "upstream-merge branch not found; skipping policy cleanup"; exit 0 - fi - wt=.wt-upstream-merge-clean - rm -rf "$wt" && git worktree add -f "$wt" origin/upstream-merge - pushd "$wt" >/dev/null - removed=false - # Remove any accidentally committed merge artifacts from repo root - for f in MERGE_PLAN.md MERGE_REPORT.md; do - if git ls-files -- "$f" | grep -q .; then git rm -f -- "$f"; removed=true; fi - done - # Remove any accidentally committed artifacts under .github/auto/** (these should be uploaded, not tracked) - auto_tracked=$(git ls-files -- '.github/auto/**' || true) - if [ -n "$auto_tracked" ]; then echo "$auto_tracked" | xargs -r git rm -f --; removed=true; fi - # Remove any tracked upstream images disallowed by local policy - for p in .github/codex-cli-*.png .github/codex-cli-*.jpg .github/codex-cli-*.jpeg .github/codex-cli-*.webp; do - files=$(git ls-files -- "$p" || true) - if [ -n "$files" ]; then echo "$files" | xargs -r git rm -f --; removed=true; fi - done - # Belt-and-suspenders: drop any accidentally committed cargo cache dirs - # Cover both repo-root and nested workspace (e.g., codex-rs/.cargo-home) - for d in .cargo-home .cargo2 codex-rs/.cargo-home codex-rs/.cargo2; do - files=$(git ls-files -- "$d/**" || true) - if [ -n "$files" ]; then echo "$files" | xargs -r git rm -f --; removed=true; fi - done - # Do NOT drop new upstream crates automatically. Let the agent decide with context. - # Apply purge_globs and perma_removed_paths from merge-policy.json when present - if command -v jq >/dev/null 2>&1 && [ -f ".github/merge-policy.json" ]; then - mapfile -t purges < <(jq -r '.purge_globs[]? // empty' .github/merge-policy.json 2>/dev/null || true) - mapfile -t perma < <(jq -r '.perma_removed_paths[]? // empty' .github/merge-policy.json 2>/dev/null || true) - for pat in "${purges[@]}" "${perma[@]}"; do - [ -n "${pat:-}" ] || continue - files=$(git ls-files -- "$pat" || true) - if [ -n "$files" ]; then echo "$files" | xargs -r git rm -f --; removed=true; fi - done - fi - # Do NOT automatically remove reintroduced paths; the agent reviews and decides. - if [ "$removed" = true ]; then - git -c user.email="github-actions[bot]@users.noreply.github.com" -c user.name="github-actions[bot]" \ - commit -m "chore(merge): enforce policy removals (images + cargo caches)" - git push origin HEAD:upstream-merge - fi - popd >/dev/null - git worktree remove -f "$wt" - - - name: Branding report (guide-only) and perma-deleted guard - if: steps.check_upstream.outputs.skip != 'true' - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - run: | - set -euo pipefail - # Guide-only branding report: detect user-visible 'Codex' strings under TUI/CLI affected by this merge. - mkdir -p .github/auto - : > .github/auto/VERIFY_branding.log - changed_files=$(git diff --name-only "origin/${DEFAULT_BRANCH}..origin/upstream-merge" -- 'codex-rs/tui/**' 'codex-cli/**' | tr '\n' ' ' || true) - if [ -n "${changed_files:-}" ]; then - echo "[branding] scanning changed TUI/CLI files for user-visible 'Codex' strings..." | tee -a .github/auto/VERIFY_branding.log - git diff -U0 --no-color "origin/${DEFAULT_BRANCH}..origin/upstream-merge" -- $changed_files \ - | grep -E '^\+' \ - | grep -E '"[^"]*Codex[^"]*"|'\''[^'\''']*Codex[^'\''']*'\''|`[^`]*Codex[^`]*`' \ - | grep -Evi '(codex-rs|codex-[a-z0-9_-]+|https?://|Cargo|crate|package|workspace)' \ - | sed 's/^/+ /' | tee -a .github/auto/VERIFY_branding.log || true - echo "[branding] Note: guidance only. No changes applied or committed." | tee -a .github/auto/VERIFY_branding.log - else - echo "[branding] no TUI/CLI files changed vs origin/${DEFAULT_BRANCH}." | tee -a .github/auto/VERIFY_branding.log - fi - # docs/** changes are allowed; we may still prefer ours but do not hard-fail here. - if git diff --name-only "origin/${DEFAULT_BRANCH}..origin/upstream-merge" -- 'docs/**' | grep -q .; then - echo "::notice::docs/** changed on upstream-merge; ensuring agent reviews and preserves local branding where needed." >&2 - fi - # Perma-deleted guard: ensure none of perma_removed_paths exist on branch - if command -v jq >/dev/null 2>&1 && [ -f ".github/merge-policy.json" ]; then - mapfile -t perma < <(jq -r '.perma_removed_paths[]? // empty' .github/merge-policy.json 2>/dev/null || true) - if [ ${#perma[@]} -gt 0 ]; then - wt=.wt-guard - rm -rf "$wt" && git worktree add -f "$wt" origin/upstream-merge >/dev/null - pushd "$wt" >/dev/null - for pat in "${perma[@]}"; do - [ -n "${pat:-}" ] || continue - if git ls-files -- "$pat" | grep -q .; then - echo "Perma-deleted guard: files present matching '$pat'" >&2 - git ls-files -- "$pat" >&2 || true - exit 1 - fi - done - popd >/dev/null - git worktree remove -f "$wt" >/dev/null - fi - fi - # Perma-deleted guard: ensure none of perma_removed_paths exist on branch - if command -v jq >/dev/null 2>&1 && [ -f ".github/merge-policy.json" ]; then - mapfile -t perma < <(jq -r '.perma_removed_paths[]? // empty' .github/merge-policy.json 2>/dev/null || true) - if [ ${#perma[@]} -gt 0 ]; then - wt=.wt-guard - rm -rf "$wt" && git worktree add -f "$wt" origin/upstream-merge >/dev/null - pushd "$wt" >/dev/null - for pat in "${perma[@]}"; do - [ -n "${pat:-}" ] || continue - if git ls-files -- "$pat" | grep -q .; then - echo "Perma-deleted guard: files present matching '$pat'" >&2 - git ls-files -- "$pat" >&2 || true - exit 1 - fi - done - popd >/dev/null - git worktree remove -f "$wt" >/dev/null - fi - fi - - - name: TUI invariants guard (strict stream ordering keys) - if: steps.check_upstream.outputs.skip != 'true' - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - run: | - set -euo pipefail - # Only run if upstream delta touched TUI - if ! grep -q '^codex-rs/tui/' .github/auto/DELTA_FILES.txt 2>/dev/null; then - echo "No TUI changes in upstream delta; skipping invariants guard."; exit 0 - fi - # Verify critical ordering identifiers exist somewhere under codex-rs/tui/ - failed=0 - for token in request_ordinal output_index sequence_number; do - if ! git ls-tree -r --name-only origin/upstream-merge -- 'codex-rs/tui/**' | while read -r f; do git show "origin/upstream-merge:$f" || true; done | grep -q "$token"; then - echo "::warning::Invariant token '$token' not found under codex-rs/tui on upstream-merge (non-blocking)." >&2 - fi - done - - - name: Summarize run - if: steps.check_upstream.outputs.skip != 'true' - env: - MERGE_MODE: ${{ steps.prep.outputs.merge_mode || 'one-shot' }} - NO_MERGE_BASE: ${{ steps.prep.outputs.no_merge_base || 'false' }} - run: | - set -euo pipefail - echo "### Upstream Merge Summary" >> "$GITHUB_STEP_SUMMARY" - echo "- Mode: ${MERGE_MODE}" >> "$GITHUB_STEP_SUMMARY" - echo "- Files changed upstream: ${{ steps.prep.outputs.files_count }} (est. LOC: ${{ steps.prep.outputs.loc_est }})" >> "$GITHUB_STEP_SUMMARY" - echo "- Branch: upstream-merge" >> "$GITHUB_STEP_SUMMARY" - echo "- Artifacts: COMMITS.json, MERGE_PLAN.md, MERGE_REPORT.md, DIFFSTAT.txt" >> "$GITHUB_STEP_SUMMARY" - if [ "${NO_MERGE_BASE}" = "true" ]; then echo "- Note: upstream/default have no merge-base (unrelated histories)." >> "$GITHUB_STEP_SUMMARY"; fi - # Preview small artifacts inline for quick debugging - for f in COMMITS.json CHANGE_HISTOGRAM.txt DIFFSTAT.txt REINTRODUCED_PATHS.txt; do - p=".github/auto/$f"; [ -s "$p" ] || continue; echo "\n#### $f" >> "$GITHUB_STEP_SUMMARY"; echo '\n```' >> "$GITHUB_STEP_SUMMARY"; sed -n '1,120p' "$p" >> "$GITHUB_STEP_SUMMARY"; echo '```' >> "$GITHUB_STEP_SUMMARY"; done - - # Upload each artifact separately so they appear as individual items in the UI - - name: Upload artifact - COMMITS.json - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: COMMITS.json - if-no-files-found: warn - path: .github/auto/COMMITS.json - - - name: Upload artifact - DELTA_FILES.txt - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: DELTA_FILES.txt - if-no-files-found: warn - path: .github/auto/DELTA_FILES.txt - - - name: Upload artifact - DIFFSTAT.txt - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: DIFFSTAT.txt - if-no-files-found: warn - path: .github/auto/DIFFSTAT.txt - - - name: Upload artifact - CHANGE_HISTOGRAM.txt - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: CHANGE_HISTOGRAM.txt - if-no-files-found: warn - path: .github/auto/CHANGE_HISTOGRAM.txt - - - name: Upload artifact - DELETED_ON_DEFAULT.txt - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: DELETED_ON_DEFAULT.txt - if-no-files-found: warn - path: .github/auto/DELETED_ON_DEFAULT.txt - - - name: Upload artifact - REINTRODUCED_PATHS.txt - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: REINTRODUCED_PATHS.txt - if-no-files-found: warn - path: .github/auto/REINTRODUCED_PATHS.txt - - - name: Upload artifact - MERGE_PLAN.md - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: MERGE_PLAN.md - if-no-files-found: warn - path: .github/auto/MERGE_PLAN.md - - - name: Upload artifact - MERGE_REPORT.md - if: steps.check_upstream.outputs.skip != 'true' - uses: actions/upload-artifact@v4 - with: - name: MERGE_REPORT.md - if-no-files-found: warn - path: .github/auto/MERGE_REPORT.md - - name: Open or update PR (use agent-supplied title/body if present) - uses: actions/github-script@v7 - env: - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }} - UPSTREAM_BRANCH: ${{ env.UPSTREAM_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - with: - github-token: ${{ secrets.CODE_GH_PAT || github.token }} - script: | - const fs = require('fs'); - function readOrDefault(p, dflt) { try { const t = fs.readFileSync(p,'utf8').trim(); return t || dflt; } catch { return dflt; } } - const owner = context.repo.owner; - const repo = context.repo.repo; - const head = process.env.MERGE_BRANCH; - const base = process.env.DEFAULT_BRANCH; - const dfltTitle = `Upstream merge: ${process.env.UPSTREAM_REPO}@${process.env.UPSTREAM_BRANCH} into ${base}`; - const title = readOrDefault('.github/auto/PR_TITLE.txt', dfltTitle); - let body = readOrDefault('.github/auto/PR_BODY.md', ''); - if (!body) { - body = `This PR merges ${process.env.UPSTREAM_REPO}@${process.env.UPSTREAM_BRANCH} into ${base}.`; - } - // Ensure the branch exists on origin before creating/updating a PR. - const ref = `heads/${head}`; - try { - await github.rest.git.getRef({ owner, repo, ref }); - } catch (e) { - core.notice(`Branch '${head}' not found on origin; skipping PR creation.`); - return; - } - // Skip PR if there are no net file changes vs base. Close existing PR if present. - let zeroDiff = false; - try { - const cmp = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${base}...${head}` }); - const files = cmp.data.files || []; - // Consider zero-diff if no files changed. This handles merges that add commits but no file deltas. - zeroDiff = files.length === 0; - } catch (e) { - core.warning(`Compare failed (${base}...${head}): ${e.message}. Proceeding to PR creation.`); - } - const headRef = `${owner}:${head}`; - const prs = await github.rest.pulls.list({ owner, repo, state: 'open', head: headRef }); - if (zeroDiff) { core.notice('Zero diff vs base; skipping PR creation.'); return; } - if (prs.data.length) { - const pr = prs.data[0]; - await github.rest.pulls.update({ owner, repo, pull_number: pr.number, title, body }); - core.notice(`Updated PR #${pr.number}`); - } else { - const pr = await github.rest.pulls.create({ owner, repo, title, head: headRef, base, body }); - core.notice(`Created PR #${pr.data.number}`); - } - - pr: - name: Open/Update PR (no merge work) - needs: [precheck] - if: needs.precheck.outputs.skip_due_to_active != 'true' && needs.precheck.outputs.action == 'pr_only' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out repository (full history) - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - name: Open or update PR (use agent-supplied title/body if present) - uses: actions/github-script@v7 - env: - MERGE_BRANCH: ${{ env.MERGE_BRANCH }} - UPSTREAM_REPO: ${{ env.UPSTREAM_REPO }} - UPSTREAM_BRANCH: ${{ env.UPSTREAM_BRANCH }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} - with: - github-token: ${{ secrets.CODE_GH_PAT || github.token }} - script: | - const fs = require('fs'); - function readOrDefault(p, dflt) { try { const t = fs.readFileSync(p,'utf8').trim(); return t || dflt; } catch { return dflt; } } - const owner = context.repo.owner; - const repo = context.repo.repo; - const head = process.env.MERGE_BRANCH; - const base = process.env.DEFAULT_BRANCH; - const dfltTitle = `Upstream merge: ${process.env.UPSTREAM_REPO}@${process.env.UPSTREAM_BRANCH} into ${base}`; - const title = readOrDefault('.github/auto/PR_TITLE.txt', dfltTitle); - let body = readOrDefault('.github/auto/PR_BODY.md', ''); - if (!body) { body = `This PR merges ${process.env.UPSTREAM_REPO}@${process.env.UPSTREAM_BRANCH} into ${base}.`; } - const ref = `heads/${head}`; - try { await github.rest.git.getRef({ owner, repo, ref }); } catch (e) { - core.notice(`Branch '${head}' not found on origin; skipping PR creation.`); return; - } - let zeroDiff = false; - try { - const cmp = await github.rest.repos.compareCommitsWithBasehead({ owner, repo, basehead: `${base}...${head}` }); - zeroDiff = (cmp.data.files || []).length === 0; - } catch (e) { - core.warning(`Compare failed (${base}...${head}): ${e.message}. Proceeding to PR creation.`); - } - if (zeroDiff) { core.notice('Zero diff vs base; skipping PR creation.'); return; } - const headRef = `${owner}:${head}`; - const prs = await github.rest.pulls.list({ owner, repo, state: 'open', head: headRef }); - if (prs.data.length) { - const pr = prs.data[0]; - await github.rest.pulls.update({ owner, repo, pull_number: pr.number, title, body }); - core.notice(`Updated PR #${pr.number}`); - } else { - const pr = await github.rest.pulls.create({ owner, repo, title, head: headRef, base, body }); - core.notice(`Created PR #${pr.data.number}`); - } diff --git a/.github/workflows/upstream-sync.yml b/.github/workflows/upstream-sync.yml index 6bfdd0d13b64..27c2663f6be3 100644 --- a/.github/workflows/upstream-sync.yml +++ b/.github/workflows/upstream-sync.yml @@ -1,36 +1,47 @@ -# Periodic upstream sync. Fetches `just-every/code` (the canonical fork base -# Hanzo dev tracks), attempts a merge into `merge-upstream-updates`, opens -# a PR for review on conflict, auto-merges on clean. +# Periodic upstream sync. # -# Hanzo overlay paths are kept distinct from upstream so most syncs are clean. -# Conflicting paths are flagged for manual resolution; the bot does NOT -# resolve content conflicts — those need eyes. +# Fetches upstream, opens a DRAFT PR with the merge result on a branch named +# `upstream-sync/`. NEVER auto-merges. On conflict the workflow +# pushes the in-progress merge (with conflict markers committed) so a human +# can `git checkout` the branch, resolve, force-push, mark ready. # -# Trigger: every 6 hours on schedule, plus manual via workflow_dispatch. +# One way: this is the only upstream sync entry point for this repo. Do not +# add a second workflow. name: upstream-sync on: schedule: - - cron: '0 */6 * * *' + # Weekly, Monday 06:00 UTC. Codex moves fast, but a weekly cadence keeps + # PR noise low and gives a human time to review/land each batch. + - cron: '0 6 * * 1' workflow_dispatch: inputs: - upstream_remote: - description: 'Upstream remote URL' + upstream_url: + description: 'Upstream git URL' required: false - default: 'https://github.com/just-every/code.git' + default: 'https://github.com/openai/codex.git' upstream_branch: - description: 'Upstream branch to sync from' + description: 'Upstream branch' required: false default: 'main' permissions: contents: write pull-requests: write + issues: write + +concurrency: + group: upstream-sync + cancel-in-progress: false jobs: sync: runs-on: ubuntu-latest + timeout-minutes: 30 + env: + UPSTREAM_URL: ${{ inputs.upstream_url || 'https://github.com/openai/codex.git' }} + UPSTREAM_BRANCH: ${{ inputs.upstream_branch || 'main' }} steps: - name: checkout main uses: actions/checkout@v4 @@ -44,83 +55,107 @@ jobs: git config user.email 'hanzo-upstream-sync@users.noreply.github.com' - name: fetch upstream - env: - UPSTREAM_URL: ${{ github.event.inputs.upstream_remote || 'https://github.com/just-every/code.git' }} - UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} run: | - git remote add upstream "$UPSTREAM_URL" - git fetch upstream "$UPSTREAM_BRANCH" --tags --no-recurse-submodules + git remote add upstream "$UPSTREAM_URL" 2>/dev/null || git remote set-url upstream "$UPSTREAM_URL" + git fetch upstream "$UPSTREAM_BRANCH" --no-tags --no-recurse-submodules - - name: short-circuit if already in sync - id: sync_check - env: - UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + - name: short-circuit if in sync + id: behind run: | BEHIND=$(git rev-list --count "main..upstream/$UPSTREAM_BRANCH") - echo "behind=$BEHIND" >> "$GITHUB_OUTPUT" - if [ "$BEHIND" = "0" ]; then - echo "Already at upstream HEAD; nothing to do." - fi + echo "count=$BEHIND" >> "$GITHUB_OUTPUT" + if [ "$BEHIND" = "0" ]; then echo "Already at upstream HEAD."; fi - - name: prepare merge branch - if: steps.sync_check.outputs.behind != '0' - env: - UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + - name: create sync branch + if: steps.behind.outputs.count != '0' run: | - BRANCH="merge-upstream-$(date -u +%Y%m%d-%H%M%S)" - echo "MERGE_BRANCH=$BRANCH" >> "$GITHUB_ENV" + DATE=$(date -u +%Y-%m-%d) + BRANCH="upstream-sync/$DATE" + # If a branch from earlier today exists, suffix with run id to keep it unique. + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + BRANCH="$BRANCH-${{ github.run_id }}" + fi + echo "BRANCH=$BRANCH" >> "$GITHUB_ENV" git checkout -b "$BRANCH" main - name: attempt merge id: merge - if: steps.sync_check.outputs.behind != '0' - env: - UPSTREAM_BRANCH: ${{ github.event.inputs.upstream_branch || 'main' }} + if: steps.behind.outputs.count != '0' run: | set +e - git merge --no-edit "upstream/$UPSTREAM_BRANCH" -m "merge upstream/$UPSTREAM_BRANCH" - STATUS=$? - if [ "$STATUS" -ne 0 ]; then - CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ' ') - git merge --abort - git checkout main - echo "status=conflict" >> "$GITHUB_OUTPUT" - echo "conflicts=$CONFLICTS" >> "$GITHUB_OUTPUT" + git merge --no-edit --no-ff "upstream/$UPSTREAM_BRANCH" \ + -m "merge: upstream/$UPSTREAM_BRANCH ($(date -u +%Y-%m-%d))" + MERGE_STATUS=$? + if [ "$MERGE_STATUS" -ne 0 ]; then + CONFLICTS=$(git diff --name-only --diff-filter=U) + # Commit the conflict markers as-is so the branch is pushable and + # a human can `git checkout` and resolve in place. NEVER auto-resolve. + git add -A + git commit --no-verify -m "merge: upstream/$UPSTREAM_BRANCH WITH CONFLICTS — needs human resolution" + { + echo 'status=conflict' + echo 'conflicts<> "$GITHUB_OUTPUT" else - echo "status=clean" >> "$GITHUB_OUTPUT" + echo 'status=clean' >> "$GITHUB_OUTPUT" fi exit 0 - - name: push merge branch (clean) - if: steps.merge.outputs.status == 'clean' - run: | - git push origin "$MERGE_BRANCH" + - name: push sync branch + if: steps.behind.outputs.count != '0' + run: git push origin "$BRANCH" - - name: open PR (clean — auto-merge eligible) + - name: open draft PR (clean) if: steps.merge.outputs.status == 'clean' - uses: peter-evans/create-pull-request@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - branch: ${{ env.MERGE_BRANCH }} - base: main - title: 'sync: upstream just-every/code (clean)' - body: | - Automated upstream sync. Merge produced no conflicts. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr create \ + --draft \ + --base main \ + --head "$BRANCH" \ + --title "sync: upstream $UPSTREAM_URL@$UPSTREAM_BRANCH ($(date -u +%Y-%m-%d))" \ + --label upstream-sync \ + --body "Automated upstream sync. Merge produced no conflicts. - Behind by ${{ steps.sync_check.outputs.behind }} commits before this PR. +Behind by ${{ steps.behind.outputs.count }} commits before this PR. - Auto-merge will land this once required checks pass. - labels: upstream-sync, automerge +This PR is intentionally a **draft**. A human MUST review and mark ready before merge. Auto-merge is disabled by policy." - - name: open PR (conflict — needs human) + - name: open draft PR (conflict) if: steps.merge.outputs.status == 'conflict' - run: | - # Push only the marker branch (no commits beyond main); the PR body documents - # conflicting paths so a human can drive the resolution locally. - git push origin "$MERGE_BRANCH" || true - gh issue create \ - --title "upstream-sync: conflict on $(date -u +%Y-%m-%d)" \ - --label upstream-sync,conflict \ - --body "Automated upstream sync hit conflicts. Behind by ${{ steps.sync_check.outputs.behind }} commits. Conflicting paths:\n\n\`\`\`\n${{ steps.merge.outputs.conflicts }}\n\`\`\`\n\nResolve locally:\n\`\`\`\ngit fetch origin\ngit checkout main\ngit merge upstream/${{ github.event.inputs.upstream_branch || 'main' }}\n# resolve conflicts\ngit push\n\`\`\`" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CONFLICTS: ${{ steps.merge.outputs.conflicts }} + run: | + gh pr create \ + --draft \ + --base main \ + --head "$BRANCH" \ + --title "sync: upstream $UPSTREAM_URL@$UPSTREAM_BRANCH — CONFLICTS ($(date -u +%Y-%m-%d))" \ + --label upstream-sync,conflict \ + --body "Automated upstream sync hit conflicts. The merge commit was pushed **with conflict markers committed** so this branch is checkoutable. + +Behind by ${{ steps.behind.outputs.count }} commits. + +## Conflicts + +\`\`\` +$CONFLICTS +\`\`\` + +## Resolve locally + +\`\`\` +git fetch origin +git checkout $BRANCH +# Files have conflict markers committed. Resolve, then: +git add -A +git commit --amend --no-edit # or new commit, your call +git push --force-with-lease origin $BRANCH +gh pr ready # mark this PR ready for review +\`\`\` + +Auto-resolution is disabled by policy. A human must drive this." diff --git a/LLM.md b/LLM.md index e6fbe665d1e1..e39c932814cc 100644 --- a/LLM.md +++ b/LLM.md @@ -109,18 +109,31 @@ just log [args] # Tail state SQLite logs ## Merge Strategy (from upstream) -1. `git fetch openai` (remote points to local clone at `/Users/z/work/openai/codex`) -2. `git merge openai/main` — resolve conflicts preserving Hanzo branding -3. Common conflict areas: `justfile`, `package.json`, hooks, TUI module names -4. Upstream renames to watch: `multi_agents` → `collab`, `HookResult` → `HookOutcome` -5. After merge: `cargo check --manifest-path codex-rs/Cargo.toml` to verify -6. Then fix any compile errors in hanzo-dev workspace that depend on codex-rs types +Upstream is `openai/codex`. Sync is automated via `.github/workflows/upstream-sync.yml` +(weekly, Mon 06:00 UTC, also `workflow_dispatch`). The workflow: + +1. fetches `upstream/main` +2. merges into a fresh `upstream-sync/` branch +3. opens a **draft** PR — never auto-merges +4. on conflict, commits the merge with conflict markers in place and opens a draft PR + labelled `upstream-sync,conflict` listing the paths + +There is exactly one upstream sync workflow. Do not add a second. + +Manual resolution recipe when CI flags a conflict: + +1. `git fetch origin && git checkout upstream-sync/` +2. resolve conflicts; common areas: `justfile`, `package.json`, hooks, TUI module names +3. upstream renames to watch: `multi_agents` → `collab`, `HookResult` → `HookOutcome` +4. `cargo check --manifest-path codex-rs/Cargo.toml` +5. fix any hanzo-dev compile errors that depend on codex-rs types +6. `git push --force-with-lease && gh pr ready` **Remotes:** - `origin` — `git@github.com:hanzoai/dev.git` -- `openai` — `/Users/z/work/openai/codex` (local mirror) -- `openai-upstream` — `https://github.com/openai/codex.git` +- `openai` — `/Users/z/work/openai/codex` (local mirror, dev-only convenience) +- `upstream` — `https://github.com/openai/codex.git` (canonical sync source) ## npm Distribution