diff --git a/.github/workflows/autoloop.md b/.github/workflows/autoloop.md index ba33d21..4cd407b 100644 --- a/.github/workflows/autoloop.md +++ b/.github/workflows/autoloop.md @@ -283,7 +283,6 @@ Examples: 4. **Rejected or errored iterations** do not commit — changes are discarded. 5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR. 6. The branch may be **merged into the default branch** at any time (by a maintainer or CI). After merging, the branch continues to be used for future iterations — it is never deleted while the program is active. On the next iteration, the branch is automatically reset to the default branch (see step 2) so that already-merged commits do not cause patch conflicts. -7. A **sync workflow** automatically merges the default branch into all active `autoloop/*` branches whenever the default branch changes, keeping them up to date. ### Cross-Linking diff --git a/.github/workflows/sync-branches.lock.yml b/.github/workflows/sync-branches.lock.yml deleted file mode 100644 index 6972f09..0000000 --- a/.github/workflows/sync-branches.lock.yml +++ /dev/null @@ -1,550 +0,0 @@ -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# This file was automatically generated by gh-aw (v0.65.6). DO NOT EDIT. -# -# To update this file, edit the corresponding .md file and run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# Keeps Autoloop program branches up to date with the default branch. -# Runs whenever the default branch changes and merges it into all active -# autoloop/* branches so that program iterations always build on the latest code. -# -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7347655448caf952972200853ace37356f693514cb8a4ae018797501b79c86a5","compiler_version":"v0.65.6","strict":true,"agent_id":"copilot"} - -name: "Sync Branches" -"on": - push: - branches: - - main - workflow_dispatch: - inputs: - aw_context: - default: "" - description: Agent caller context (used internally by Agentic Workflows). - required: false - type: string - -permissions: {} - -concurrency: - group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}" - -run-name: "Sync Branches" - -jobs: - activation: - needs: pre_activation - if: needs.pre_activation.outputs.activated == 'true' - runs-on: ubuntu-slim - permissions: - contents: read - outputs: - comment_id: "" - comment_repo: "" - lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} - model: ${{ steps.generate_aw_info.outputs.model }} - secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Generate agentic run info - id: generate_aw_info - env: - GH_AW_INFO_ENGINE_ID: "copilot" - GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || 'auto' }} - GH_AW_INFO_VERSION: "latest" - GH_AW_INFO_AGENT_VERSION: "latest" - GH_AW_INFO_CLI_VERSION: "v0.65.6" - GH_AW_INFO_WORKFLOW_NAME: "Sync Branches" - GH_AW_INFO_EXPERIMENTAL: "false" - GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" - GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]' - GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.25.11" - GH_AW_INFO_AWMG_VERSION: "" - GH_AW_INFO_FIREWALL_TYPE: "squid" - GH_AW_COMPILED_STRICT: "true" - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); - await main(core, context); - - name: Validate COPILOT_GITHUB_TOKEN secret - id: validate-secret - run: ${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default - env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - name: Checkout .github and .agents folders - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - sparse-checkout: | - .github - .agents - sparse-checkout-cone-mode: true - fetch-depth: 1 - - name: Check workflow file timestamps - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_WORKFLOW_FILE: "sync-branches.lock.yml" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); - await main(); - - name: Check compile-agentic version - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_COMPILED_VERSION: "v0.65.6" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); - await main(); - - name: Create prompt with built-in context - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - # poutine:ignore untrusted_checkout_exec - run: | - bash ${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh - { - cat << 'GH_AW_PROMPT_4db17a6c15417a22_EOF' - - GH_AW_PROMPT_4db17a6c15417a22_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" - cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" - cat << 'GH_AW_PROMPT_4db17a6c15417a22_EOF' - - The following GitHub context information is available for this workflow: - {{#if __GH_AW_GITHUB_ACTOR__ }} - - **actor**: __GH_AW_GITHUB_ACTOR__ - {{/if}} - {{#if __GH_AW_GITHUB_REPOSITORY__ }} - - **repository**: __GH_AW_GITHUB_REPOSITORY__ - {{/if}} - {{#if __GH_AW_GITHUB_WORKSPACE__ }} - - **workspace**: __GH_AW_GITHUB_WORKSPACE__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }} - - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }} - - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }} - - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ - {{/if}} - {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }} - - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__ - {{/if}} - {{#if __GH_AW_GITHUB_RUN_ID__ }} - - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ - {{/if}} - - - GH_AW_PROMPT_4db17a6c15417a22_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_prompt.md" - cat << 'GH_AW_PROMPT_4db17a6c15417a22_EOF' - - {{#runtime-import .github/workflows/sync-branches.md}} - GH_AW_PROMPT_4db17a6c15417a22_EOF - } > "$GH_AW_PROMPT" - - name: Interpolate variables and render templates - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); - await main(); - - name: Substitute placeholders - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_GITHUB_ACTOR: ${{ github.actor }} - GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }} - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }} - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }} - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} - GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} - GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} - GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); - - // Call the substitution function - return await substitutePlaceholders({ - file: process.env.GH_AW_PROMPT, - substitutions: { - GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, - GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID, - GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER, - GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER, - GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER, - GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, - GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, - GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED - } - }); - - name: Validate prompt placeholders - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash ${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh - - name: Print prompt - env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - # poutine:ignore untrusted_checkout_exec - run: bash ${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh - - name: Upload activation artifact - if: success() - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: activation - path: | - /tmp/gh-aw/aw_info.json - /tmp/gh-aw/aw-prompts/prompt.txt - retention-days: 1 - - agent: - needs: activation - runs-on: ubuntu-latest - permissions: - contents: write - env: - GH_AW_WORKFLOW_ID_SANITIZED: syncbranches - outputs: - checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} - effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} - inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }} - model: ${{ needs.activation.outputs.model }} - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - name: Create gh-aw temp directory - run: bash ${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh - - name: Configure gh CLI for GitHub Enterprise - run: bash ${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh - env: - GH_TOKEN: ${{ github.token }} - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - GITHUB_REPOSITORY: ${{ github.repository }} - name: Merge default branch into all autoloop program branches - run: "python3 - << 'PYEOF'\nimport os, subprocess, sys\n\ntoken = os.environ.get(\"GITHUB_TOKEN\", \"\")\nrepo = os.environ.get(\"GITHUB_REPOSITORY\", \"\")\ndefault_branch = os.environ.get(\"DEFAULT_BRANCH\", \"main\")\n\n# List all remote branches matching the autoloop/* pattern\nresult = subprocess.run(\n [\"git\", \"branch\", \"-r\", \"--list\", \"origin/autoloop/*\"],\n capture_output=True, text=True\n)\nif result.returncode != 0:\n print(f\"Failed to list remote branches: {result.stderr}\")\n sys.exit(0)\n\nbranches = [b.strip().replace(\"origin/\", \"\") for b in result.stdout.strip().split(\"\\n\") if b.strip()]\n\nif not branches:\n print(\"No autoloop/* branches found. Nothing to sync.\")\n sys.exit(0)\n\nprint(f\"Found {len(branches)} autoloop branch(es) to sync: {branches}\")\n\ndef rev_count(range_spec):\n r = subprocess.run(\n [\"git\", \"rev-list\", \"--count\", range_spec],\n capture_output=True, text=True\n )\n if r.returncode != 0:\n return None\n try:\n return int(r.stdout.strip())\n except ValueError:\n return None\n\nfailed = []\nfor branch in branches:\n print(f\"\\n--- Syncing {branch} with {default_branch} ---\")\n\n # Fetch both branches so the ahead/behind counts below are computed\n # against up-to-date local copies of the remote tips.\n subprocess.run([\"git\", \"fetch\", \"origin\", branch], capture_output=True)\n subprocess.run([\"git\", \"fetch\", \"origin\", default_branch], capture_output=True)\n\n # Compute ahead/behind counts using the remote-tracking refs so we\n # make a decision based on commit delta (not content delta).\n ahead = rev_count(f\"origin/{default_branch}..origin/{branch}\")\n behind = rev_count(f\"origin/{branch}..origin/{default_branch}\")\n if ahead is None or behind is None:\n print(f\" Failed to compute ahead/behind for {branch}\")\n failed.append(branch)\n continue\n print(f\" ahead={ahead} behind={behind}\")\n\n if ahead == 0 and behind > 0:\n # All of the branch's commits are already in the default branch.\n # Merging would produce a noisy \"Merge main into branch\" commit\n # that re-exposes every historical file as a patch touch — the\n # failure mode that triggers gh-aw's E003 (>100 files) when a\n # new PR is opened. Fast-forward the canonical branch instead.\n # This is lossless because ahead=0 proves every commit on the\n # branch is already reachable from the default branch.\n ff = subprocess.run(\n [\"git\", \"checkout\", \"-B\", branch, f\"origin/{default_branch}\"],\n capture_output=True, text=True\n )\n if ff.returncode != 0:\n print(f\" Failed to fast-forward {branch}: {ff.stderr}\")\n failed.append(branch)\n continue\n # Use --force-with-lease so that if anyone else is simultaneously\n # pushing to the branch, the update is rejected rather than\n # overwriting their commits.\n push = subprocess.run(\n [\"git\", \"push\", \"--force-with-lease\", \"origin\", branch],\n capture_output=True, text=True\n )\n if push.returncode != 0:\n print(f\" Failed to force-push {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n print(f\" Fast-forwarded {branch} to origin/{default_branch}\")\n continue\n\n if ahead == 0 and behind == 0:\n # Already at default branch — nothing to do.\n print(f\" {branch} is already up to date with origin/{default_branch}\")\n continue\n\n if ahead > 0 and behind == 0:\n # Unique work preserved; no upstream drift to merge.\n print(f\" {branch} is ahead of origin/{default_branch} with no upstream drift; nothing to merge.\")\n continue\n\n # True divergence (ahead > 0 and behind > 0): check out and merge.\n checkout = subprocess.run(\n [\"git\", \"checkout\", \"-B\", branch, f\"origin/{branch}\"],\n capture_output=True, text=True\n )\n if checkout.returncode != 0:\n print(f\" Failed to checkout {branch}: {checkout.stderr}\")\n failed.append(branch)\n continue\n\n # Merge the default branch into the program branch\n merge = subprocess.run(\n [\"git\", \"merge\", f\"origin/{default_branch}\", \"--no-edit\",\n \"-m\", f\"Merge {default_branch} into {branch}\"],\n capture_output=True, text=True\n )\n if merge.returncode != 0:\n print(f\" Merge conflict or failure for {branch}: {merge.stderr}\")\n # Abort the merge to leave a clean state\n subprocess.run([\"git\", \"merge\", \"--abort\"], capture_output=True)\n failed.append(branch)\n continue\n\n # Push the updated branch\n push = subprocess.run(\n [\"git\", \"push\", \"origin\", branch],\n capture_output=True, text=True\n )\n if push.returncode != 0:\n print(f\" Failed to push {branch}: {push.stderr}\")\n failed.append(branch)\n continue\n\n print(f\" Successfully synced {branch}\")\n\n# Return to default branch\nsubprocess.run([\"git\", \"checkout\", default_branch], capture_output=True)\n\nif failed:\n print(f\"\\n⚠️ Failed to sync {len(failed)} branch(es): {failed}\")\n print(\"These branches may need manual conflict resolution.\")\n # Don't fail the workflow — log the issue but continue\nelse:\n print(f\"\\n✅ All {len(branches)} branch(es) synced successfully.\")\nPYEOF" - - name: Checkout PR branch - id: checkout-pr - if: | - github.event.pull_request || github.event.issue.pull_request - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - with: - github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); - await main(); - - name: Install GitHub Copilot CLI - run: ${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh latest - - name: Install AWF binary - run: bash ${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh v0.25.11 - - name: Determine automatic lockdown mode for GitHub MCP Server - id: determine-automatic-lockdown - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - with: - script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); - await determineAutomaticLockdown(github, context, core); - - name: Download container images - run: bash ${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.25.11 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.11 ghcr.io/github/gh-aw-firewall/squid:0.25.11 ghcr.io/github/gh-aw-mcpg:v0.2.11 ghcr.io/github/github-mcp-server:v0.32.0 - - name: Start MCP Gateway - id: start-mcp-gateway - env: - GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - set -eo pipefail - mkdir -p /tmp/gh-aw/mcp-config - - # Export gateway environment variables for MCP config and gateway script - export MCP_GATEWAY_PORT="80" - export MCP_GATEWAY_DOMAIN="host.docker.internal" - MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') - echo "::add-mask::${MCP_GATEWAY_API_KEY}" - export MCP_GATEWAY_API_KEY - export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" - mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" - export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" - export DEBUG="*" - - export GH_AW_ENGINE="copilot" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.2.11' - - mkdir -p /home/runner/.copilot - cat << GH_AW_MCP_CONFIG_c44c901ef7ee68bd_EOF | bash ${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.sh - { - "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v0.32.0", - "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "repos" - }, - "guard-policies": { - "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" - } - } - } - }, - "gateway": { - "port": $MCP_GATEWAY_PORT, - "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", - "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" - } - } - GH_AW_MCP_CONFIG_c44c901ef7ee68bd_EOF - - name: Download activation artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: activation - path: /tmp/gh-aw - - name: Clean git credentials - continue-on-error: true - run: bash ${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh - - name: Execute GitHub Copilot CLI - id: agentic_execution - # Copilot CLI tool arguments (sorted): - timeout-minutes: 10 - run: | - set -o pipefail - touch /tmp/gh-aw/agent-step-summary.md - # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --image-tag 0.25.11 --skip-pull --enable-api-proxy \ - -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log - env: - COPILOT_AGENT_RUNNER_TYPE: STANDALONE - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || '' }} - GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json - GH_AW_PHASE: agent - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_VERSION: v0.65.6 - GITHUB_API_URL: ${{ github.api_url }} - GITHUB_AW: true - GITHUB_HEAD_REF: ${{ github.head_ref }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_REF_NAME: ${{ github.ref_name }} - GITHUB_SERVER_URL: ${{ github.server_url }} - GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md - GITHUB_WORKSPACE: ${{ github.workspace }} - GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_AUTHOR_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - XDG_CONFIG_HOME: /home/runner - - name: Detect inference access error - id: detect-inference-error - if: always() - continue-on-error: true - run: bash ${RUNNER_TEMP}/gh-aw/actions/detect_inference_access_error.sh - - name: Configure Git credentials - env: - REPO_NAME: ${{ github.repository }} - SERVER_URL: ${{ github.server_url }} - run: | - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git config --global user.name "github-actions[bot]" - git config --global am.keepcr true - # Re-authenticate git with GitHub token - SERVER_URL_STRIPPED="${SERVER_URL#https://}" - git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git" - echo "Git configured with standard GitHub Actions identity" - - name: Copy Copilot session state files to logs - if: always() - continue-on-error: true - run: bash ${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh - - name: Stop MCP Gateway - if: always() - continue-on-error: true - env: - MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} - MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} - GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} - run: | - bash ${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID" - - name: Redact secrets in logs - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); - await main(); - env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' - SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} - SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Append agent step summary - if: always() - run: bash ${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh - - name: Parse agent logs for step summary - if: always() - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); - await main(); - - name: Parse MCP Gateway logs for step summary - if: always() - id: parse-mcp-gateway - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); - await main(); - - name: Print firewall logs - if: always() - continue-on-error: true - env: - AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs - run: | - # Fix permissions on firewall logs so they can be uploaded as artifacts - # AWF runs with sudo, creating files owned by root - sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true - # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step) - if command -v awf &> /dev/null; then - awf logs summary | tee -a "$GITHUB_STEP_SUMMARY" - else - echo 'AWF binary not installed, skipping firewall log summary' - fi - - name: Parse token usage for step summary - if: always() - continue-on-error: true - run: bash ${RUNNER_TEMP}/gh-aw/actions/parse_token_usage.sh - - name: Upload agent artifacts - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: agent - path: | - /tmp/gh-aw/aw-prompts/prompt.txt - /tmp/gh-aw/sandbox/agent/logs/ - /tmp/gh-aw/redacted-urls.log - /tmp/gh-aw/mcp-logs/ - /tmp/gh-aw/agent-stdio.log - /tmp/gh-aw/agent/ - if-no-files-found: ignore - - name: Upload firewall audit logs - if: always() - continue-on-error: true - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 - with: - name: firewall-audit-logs - path: | - /tmp/gh-aw/sandbox/firewall/logs/ - /tmp/gh-aw/sandbox/firewall/audit/ - if-no-files-found: ignore - - pre_activation: - runs-on: ubuntu-slim - outputs: - activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} - matched_command: '' - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@31130b20a8fd3ef263acbe2091267c0aace07e09 # v0.65.6 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - name: Check team membership for workflow - id: check_membership - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 - env: - GH_AW_REQUIRED_ROLES: "admin,maintainer,write" - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); - await main(); - diff --git a/.github/workflows/sync-branches.md b/.github/workflows/sync-branches.md deleted file mode 100644 index 3d87064..0000000 --- a/.github/workflows/sync-branches.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -description: | - Keeps Autoloop program branches up to date with the default branch. - Runs whenever the default branch changes and merges it into all active - autoloop/* branches so that program iterations always build on the latest code. - -on: - push: - branches: [main] # ← update this if your default branch is not 'main' - workflow_dispatch: - -permissions: read-all - -timeout-minutes: 10 - -tools: - github: - toolsets: [repos] - bash: true - -steps: - - name: Merge default branch into all autoloop program branches - env: - GITHUB_REPOSITORY: ${{ github.repository }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - python3 - << 'PYEOF' - import os, subprocess, sys - - token = os.environ.get("GITHUB_TOKEN", "") - repo = os.environ.get("GITHUB_REPOSITORY", "") - default_branch = os.environ.get("DEFAULT_BRANCH", "main") - - # List all remote branches matching the autoloop/* pattern - result = subprocess.run( - ["git", "branch", "-r", "--list", "origin/autoloop/*"], - capture_output=True, text=True - ) - if result.returncode != 0: - print(f"Failed to list remote branches: {result.stderr}") - sys.exit(0) - - branches = [b.strip().replace("origin/", "") for b in result.stdout.strip().split("\n") if b.strip()] - - if not branches: - print("No autoloop/* branches found. Nothing to sync.") - sys.exit(0) - - print(f"Found {len(branches)} autoloop branch(es) to sync: {branches}") - - def rev_count(range_spec): - r = subprocess.run( - ["git", "rev-list", "--count", range_spec], - capture_output=True, text=True - ) - if r.returncode != 0: - return None - try: - return int(r.stdout.strip()) - except ValueError: - return None - - failed = [] - for branch in branches: - print(f"\n--- Syncing {branch} with {default_branch} ---") - - # Fetch both branches so the ahead/behind counts below are computed - # against up-to-date local copies of the remote tips. - subprocess.run(["git", "fetch", "origin", branch], capture_output=True) - subprocess.run(["git", "fetch", "origin", default_branch], capture_output=True) - - # Compute ahead/behind counts using the remote-tracking refs so we - # make a decision based on commit delta (not content delta). - ahead = rev_count(f"origin/{default_branch}..origin/{branch}") - behind = rev_count(f"origin/{branch}..origin/{default_branch}") - if ahead is None or behind is None: - print(f" Failed to compute ahead/behind for {branch}") - failed.append(branch) - continue - print(f" ahead={ahead} behind={behind}") - - if ahead == 0 and behind > 0: - # All of the branch's commits are already in the default branch. - # Merging would produce a noisy "Merge main into branch" commit - # that re-exposes every historical file as a patch touch — the - # failure mode that triggers gh-aw's E003 (>100 files) when a - # new PR is opened. Fast-forward the canonical branch instead. - # This is lossless because ahead=0 proves every commit on the - # branch is already reachable from the default branch. - ff = subprocess.run( - ["git", "checkout", "-B", branch, f"origin/{default_branch}"], - capture_output=True, text=True - ) - if ff.returncode != 0: - print(f" Failed to fast-forward {branch}: {ff.stderr}") - failed.append(branch) - continue - # Use --force-with-lease so that if anyone else is simultaneously - # pushing to the branch, the update is rejected rather than - # overwriting their commits. - push = subprocess.run( - ["git", "push", "--force-with-lease", "origin", branch], - capture_output=True, text=True - ) - if push.returncode != 0: - print(f" Failed to force-push {branch}: {push.stderr}") - failed.append(branch) - continue - print(f" Fast-forwarded {branch} to origin/{default_branch}") - continue - - if ahead == 0 and behind == 0: - # Already at default branch — nothing to do. - print(f" {branch} is already up to date with origin/{default_branch}") - continue - - if ahead > 0 and behind == 0: - # Unique work preserved; no upstream drift to merge. - print(f" {branch} is ahead of origin/{default_branch} with no upstream drift; nothing to merge.") - continue - - # True divergence (ahead > 0 and behind > 0): check out and merge. - checkout = subprocess.run( - ["git", "checkout", "-B", branch, f"origin/{branch}"], - capture_output=True, text=True - ) - if checkout.returncode != 0: - print(f" Failed to checkout {branch}: {checkout.stderr}") - failed.append(branch) - continue - - # Merge the default branch into the program branch - merge = subprocess.run( - ["git", "merge", f"origin/{default_branch}", "--no-edit", - "-m", f"Merge {default_branch} into {branch}"], - capture_output=True, text=True - ) - if merge.returncode != 0: - print(f" Merge conflict or failure for {branch}: {merge.stderr}") - # Abort the merge to leave a clean state - subprocess.run(["git", "merge", "--abort"], capture_output=True) - failed.append(branch) - continue - - # Push the updated branch - push = subprocess.run( - ["git", "push", "origin", branch], - capture_output=True, text=True - ) - if push.returncode != 0: - print(f" Failed to push {branch}: {push.stderr}") - failed.append(branch) - continue - - print(f" Successfully synced {branch}") - - # Return to default branch - subprocess.run(["git", "checkout", default_branch], capture_output=True) - - if failed: - print(f"\n⚠️ Failed to sync {len(failed)} branch(es): {failed}") - print("These branches may need manual conflict resolution.") - # Don't fail the workflow — log the issue but continue - else: - print(f"\n✅ All {len(branches)} branch(es) synced successfully.") - PYEOF ---- - -Sync all autoloop/* branches with the default branch. diff --git a/AGENTS.md b/AGENTS.md index 63d0475..1ee2120 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,6 @@ autoloop/ ├── AGENTS.md ← you are here ├── workflows/ ← Agentic Workflow definitions │ ├── autoloop.md ← main autoloop workflow (compiled by gh-aw) -│ ├── sync-branches.md ← syncs default branch into autoloop/* branches │ ├── shared/ ← shared workflow fragments │ │ └── reporting.md │ └── scripts/ ← standalone scripts invoked from steps @@ -72,7 +71,7 @@ The workflow (`workflows/autoloop.md`) is compiled by `gh aw compile` into `.git 6. Updates the program's state file in repo-memory with all state (Machine State table + research sections) 7. If the program has a `target-metric` and the metric is reached, marks it as completed (removes `autoloop-program` label, adds `autoloop-completed` label for issue-based programs) -A companion workflow (`workflows/sync-branches.md`) runs on every push to the default branch and merges it into all active `autoloop/*` program branches, keeping them up to date. +Branch freshness is handled by the iteration loop itself: each iteration's Step 3 (in `workflows/autoloop.md`, "Iteration Loop" → step "Branch Setup") fast-forwards or merges `origin/main` into the program's `autoloop/*` branch as needed. No separate sync workflow is required. ### Evolution Strategy @@ -134,9 +133,8 @@ Programs run on a schedule, but can also be triggered manually: To deploy the workflow to a repository: 1. Copy `workflows/autoloop.md` to `.github/workflows/autoloop.md` in the target repo -2. Copy `workflows/sync-branches.md` to `.github/workflows/sync-branches.md` in the target repo -3. Copy `workflows/shared/` to `.github/workflows/shared/` in the target repo -4. Copy `workflows/scripts/` to `.github/workflows/scripts/` in the target repo -5. Run `gh aw compile autoloop` and `gh aw compile sync-branches` to generate the lock files -6. Copy program directories to `.autoloop/programs/` in the target repo -7. Commit and push +2. Copy `workflows/shared/` to `.github/workflows/shared/` in the target repo +3. Copy `workflows/scripts/` to `.github/workflows/scripts/` in the target repo +4. Run `gh aw compile autoloop` to generate the lock file +5. Copy program directories to `.autoloop/programs/` in the target repo +6. Commit and push diff --git a/README.md b/README.md index 79773e1..bc1693d 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,6 @@ The command is handled by the `autoloop` Agentic Workflow — it is **not** a Gi autoloop/ ├── workflows/ ← Agentic Workflow definitions │ ├── autoloop.md ← the workflow (compiled by gh aw) -│ ├── sync-branches.md ← syncs default branch into autoloop/* branches │ └── shared/ │ └── reporting.md ├── .autoloop/ diff --git a/install.md b/install.md index 52923c9..04b7851 100644 --- a/install.md +++ b/install.md @@ -85,10 +85,9 @@ rm -rf /tmp/autoloop ```bash gh aw compile autoloop -gh aw compile sync-branches ``` -**What this does**: Generates `.github/workflows/autoloop.lock.yml` and `.github/workflows/sync-branches.lock.yml` from the workflow definitions. +**What this does**: Generates `.github/workflows/autoloop.lock.yml` from the workflow definition. ## Step 5: Create a Branch, Commit, and Open a Pull Request @@ -141,7 +140,7 @@ Optionally, you may copy existing examples from the [`.autoloop/programs/`](.aut ### Compile fails -- Ensure `.github/workflows/autoloop.md` and `.github/workflows/sync-branches.md` exist +- Ensure `.github/workflows/autoloop.md` exists - Ensure `.github/workflows/shared/` directory was copied - Re-run `gh aw compile autoloop` with `--verbose` for details diff --git a/tests/test_scheduling.py b/tests/test_scheduling.py index 7064391..6f04b88 100644 --- a/tests/test_scheduling.py +++ b/tests/test_scheduling.py @@ -685,92 +685,6 @@ def test_clone_before_scheduling(self): ) -class TestSyncBranchesCredentialOrdering: - """Verify that Git credentials are configured before the merge/push step. - - The sync-branches workflow merges the default branch into autoloop/* - branches. Merge commits require a Git identity (user.name/user.email) - and pushes/fetches need an authenticated remote URL. Both must be - configured before the merge step runs. - """ - - CRED_STEP = "Set up Git identity and authentication" - MERGE_STEP = "Merge default branch into all autoloop program branches" - - def _load_steps(self): - """Return the list of pre-step names from workflows/sync-branches.md.""" - import os - - wf_path = os.path.join(os.path.dirname(__file__), "..", "workflows", "sync-branches.md") - with open(wf_path) as f: - content = f.read() - step_names = [] - for m in re.finditer(r'^\s*-\s*name:\s*(.+)$', content, re.MULTILINE): - step_names.append(m.group(1).strip()) - return step_names - - def _load_lock_steps(self): - """Return the list of step names from the agent job in - .github/workflows/sync-branches.lock.yml. - - Parsed with a regex (rather than PyYAML) so the test has no - external dependencies beyond pytest. - """ - import os - - lock_path = os.path.join( - os.path.dirname(__file__), "..", ".github", "workflows", "sync-branches.lock.yml" - ) - with open(lock_path) as f: - content = f.read() - # Restrict to the 'agent:' job body so we don't pick up step names - # from other jobs (e.g. 'activation'). - agent_match = re.search(r"^ agent:\n((?: .*\n|\n)+)", content, re.MULTILINE) - if not agent_match: - return [] - agent_body = agent_match.group(1) - # Step names appear as either ' - name: ' or - # ' name: ' (when the step starts with '- env:'). - step_names = [] - for m in re.finditer(r'^\s{6,8}(?:- )?name:\s*(.+)$', agent_body, re.MULTILINE): - step_names.append(m.group(1).strip()) - return step_names - - def test_cred_step_exists(self): - """A step that configures Git identity/auth must exist in the source.""" - steps = self._load_steps() - assert self.CRED_STEP in steps, ( - f"Expected step '{self.CRED_STEP}' not found. Steps: {steps}" - ) - - def test_creds_before_merge(self): - """The credential step must come before the merge step in the source.""" - steps = self._load_steps() - cred_idx = steps.index(self.CRED_STEP) - merge_idx = steps.index(self.MERGE_STEP) - assert cred_idx < merge_idx, ( - f"'{self.CRED_STEP}' (index {cred_idx}) must come before " - f"'{self.MERGE_STEP}' (index {merge_idx}). Steps: {steps}" - ) - - def test_lock_creds_before_merge(self): - """In the compiled lock file, Configure Git credentials must come before the merge step.""" - steps = self._load_lock_steps() - cred_names = [s for s in steps if "Configure Git credentials" in s] - assert cred_names, ( - f"No 'Configure Git credentials' step found in lock file. Steps: {steps}" - ) - merge_names = [s for s in steps if "Merge default branch" in s] - assert merge_names, ( - f"No merge step found in lock file. Steps: {steps}" - ) - cred_idx = steps.index(cred_names[0]) - merge_idx = steps.index(merge_names[0]) - assert cred_idx < merge_idx, ( - f"'Configure Git credentials' (index {cred_idx}) must come before " - f"merge step (index {merge_idx}). Steps: {steps}" - ) - # --------------------------------------------------------------------------- # Single-PR-per-program invariant: safe-outputs config + existing_pr lookup diff --git a/workflows/autoloop.md b/workflows/autoloop.md index 5f14579..1b5670d 100644 --- a/workflows/autoloop.md +++ b/workflows/autoloop.md @@ -325,7 +325,6 @@ Examples: 4. **Rejected or errored iterations** do not commit — changes are discarded. 5. A **single draft PR** is created for the branch on the first accepted iteration. Future accepted iterations push additional commits to the same PR. 6. The branch may be **merged into the default branch** at any time (by a maintainer or CI). After merging, the branch continues to be used for future iterations — it is never deleted while the program is active. On the next iteration, the branch is automatically reset to the default branch (see step 2) so that already-merged commits do not cause patch conflicts. -7. A **sync workflow** automatically merges the default branch into all active `autoloop/*` branches whenever the default branch changes, keeping them up to date. ### Cross-Linking diff --git a/workflows/sync-branches.md b/workflows/sync-branches.md deleted file mode 100644 index cad1adb..0000000 --- a/workflows/sync-branches.md +++ /dev/null @@ -1,196 +0,0 @@ ---- -description: | - Keeps Autoloop program branches up to date with the default branch. - Runs whenever the default branch changes and merges it into all active - autoloop/* branches so that program iterations always build on the latest code. - -on: - push: - branches: [main] # ← update this if your default branch is not 'main' - workflow_dispatch: - -permissions: - contents: write - -timeout-minutes: 10 - -tools: - github: - toolsets: [repos] - bash: true - -steps: - - name: Set up Git identity and authentication - env: - GH_TOKEN: ${{ github.token }} - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_SERVER_URL: ${{ github.server_url }} - run: | - node - << 'JSEOF' - const { spawnSync } = require('child_process'); - function git(...args) { - const result = spawnSync('git', args, { encoding: 'utf-8' }); - if (result.status !== 0) { - console.error('git ' + args.join(' ') + ' failed: ' + result.stderr); - process.exit(1); - } - return result; - } - git('config', '--global', 'user.email', 'github-actions[bot]@users.noreply.github.com'); - git('config', '--global', 'user.name', 'github-actions[bot]'); - const ghToken = process.env.GH_TOKEN || ''; - const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com'; - const repo = process.env.GITHUB_REPOSITORY || ''; - if (ghToken && repo) { - const authUrl = serverUrl.replace('https://', 'https://x-access-token:' + ghToken + '@') + '/' + repo + '.git'; - git('remote', 'set-url', 'origin', authUrl); - } - console.log('Git identity and authentication configured.'); - JSEOF - - - name: Merge default branch into all autoloop program branches - env: - GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_TOKEN: ${{ github.token }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - node - << 'JSEOF' - const { execSync, spawnSync } = require('child_process'); - - const defaultBranch = process.env.DEFAULT_BRANCH || 'main'; - - function git(...args) { - const result = spawnSync('git', args, { encoding: 'utf-8' }); - return { returncode: result.status, stdout: result.stdout || '', stderr: result.stderr || '' }; - } - - // Discover all remote branches matching the autoloop/* pattern. - // Use ls-remote instead of 'git branch -r' so we don't depend on - // pre-fetched remote-tracking refs (shallow checkouts won't have them). - const listResult = git('ls-remote', '--heads', 'origin', 'autoloop/*'); - if (listResult.returncode !== 0) { - console.log('Failed to list remote branches: ' + listResult.stderr); - process.exit(0); - } - - const branches = listResult.stdout.trim().split('\n') - .map(b => b.trim()) - .filter(b => b) - .map(b => b.replace(/^.*refs\/heads\//, '')); - - if (branches.length === 0) { - console.log('No autoloop/* branches found. Nothing to sync.'); - process.exit(0); - } - - console.log('Found ' + branches.length + ' autoloop branch(es) to sync: ' + JSON.stringify(branches)); - - const failed = []; - for (const branch of branches) { - console.log('\n--- Syncing ' + branch + ' with ' + defaultBranch + ' ---'); - - // Fetch both branches - git('fetch', 'origin', branch); - git('fetch', 'origin', defaultBranch); - - // Compute ahead/behind counts using the remote-tracking refs so we - // make a decision based on commit delta (not content delta). - const aheadResult = git('rev-list', '--count', - 'origin/' + defaultBranch + '..origin/' + branch); - const behindResult = git('rev-list', '--count', - 'origin/' + branch + '..origin/' + defaultBranch); - if (aheadResult.returncode !== 0 || behindResult.returncode !== 0) { - console.log(' Failed to compute ahead/behind for ' + branch + ': ' + - (aheadResult.stderr || behindResult.stderr)); - failed.push(branch); - continue; - } - const ahead = parseInt((aheadResult.stdout || '0').trim(), 10) || 0; - const behind = parseInt((behindResult.stdout || '0').trim(), 10) || 0; - console.log(' ahead=' + ahead + ' behind=' + behind); - - if (ahead === 0 && behind > 0) { - // All of the branch's commits are already in the default branch. - // Merging would produce a noisy "Merge main into branch" commit - // that re-exposes every historical file as a patch touch — the - // failure mode that triggers gh-aw's E003 (>100 files) when a - // new PR is opened. Fast-forward the canonical branch instead. - // This is lossless because ahead=0 proves every commit on the - // branch is already reachable from the default branch. - const ff = git('checkout', '-B', branch, 'origin/' + defaultBranch); - if (ff.returncode !== 0) { - console.log(' Failed to fast-forward ' + branch + ': ' + ff.stderr); - failed.push(branch); - continue; - } - // Use --force-with-lease so that if anyone else is simultaneously - // pushing to the branch, the update is rejected rather than - // overwriting their commits. - const push = git('push', '--force-with-lease', 'origin', branch); - if (push.returncode !== 0) { - console.log(' Failed to force-push ' + branch + ': ' + push.stderr); - failed.push(branch); - continue; - } - console.log(' Fast-forwarded ' + branch + ' to origin/' + defaultBranch); - continue; - } - - if (ahead === 0 && behind === 0) { - // Already at default branch — nothing to do. - console.log(' ' + branch + ' is already up to date with origin/' + defaultBranch); - continue; - } - - if (ahead > 0 && behind === 0) { - // Unique work preserved; no upstream drift to merge. - console.log(' ' + branch + ' is ahead of origin/' + defaultBranch + ' with no upstream drift; nothing to merge.'); - continue; - } - - // True divergence (ahead > 0 && behind > 0): check out and merge. - let checkout = git('checkout', '-B', branch, 'origin/' + branch); - if (checkout.returncode !== 0) { - console.log(' Failed to checkout ' + branch + ': ' + checkout.stderr); - failed.push(branch); - continue; - } - - const merge = git('merge', 'origin/' + defaultBranch, '--no-edit', - '-m', 'Merge ' + defaultBranch + ' into ' + branch); - if (merge.returncode !== 0) { - console.log(' Merge conflict or failure for ' + branch + ': ' + merge.stderr); - // Abort the merge to leave a clean state - git('merge', '--abort'); - failed.push(branch); - continue; - } - - // Push the updated branch - const push = git('push', 'origin', branch); - if (push.returncode !== 0) { - console.log(' Failed to push ' + branch + ': ' + push.stderr); - failed.push(branch); - continue; - } - - console.log(' Successfully synced ' + branch); - } - - // Return to default branch - git('checkout', defaultBranch); - - if (failed.length > 0) { - console.log('\n\u26a0\ufe0f Failed to sync ' + failed.length + " branch(es): " + JSON.stringify(failed)); // \u26a0\ufe0f = warning sign - console.log('These branches may need manual conflict resolution.'); - // Don't fail the workflow -- log the issue but continue - } else { - console.log('\n\u2705 All ' + branches.length + " branch(es) synced successfully."); // \u2705 = checkmark - } - JSEOF ---- - -Sync all autoloop/* branches with the default branch.