diff --git a/.github/workflows/evergreen-trigger.yml b/.github/workflows/evergreen-trigger.yml new file mode 100644 index 00000000..22c4cecf --- /dev/null +++ b/.github/workflows/evergreen-trigger.yml @@ -0,0 +1,45 @@ +name: Evergreen Trigger + +on: + pull_request_target: + types: [labeled, synchronize, reopened, ready_for_review] + +permissions: + contents: read + pull-requests: read + +jobs: + gate: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.scope.outputs.should_run }} + pr_number: ${{ github.event.pull_request.number }} + head_sha: ${{ github.event.pull_request.head.sha }} + steps: + - id: scope + env: + LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + STATE: ${{ github.event.pull_request.state }} + run: | + if [ "$STATE" = "open" ] && echo "$LABELS" | grep -q '"evergreen"'; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + else + echo "should_run=false" >> "$GITHUB_OUTPUT" + fi + + evergreen: + needs: gate + if: needs.gate.outputs.should_run == 'true' + permissions: + actions: write + checks: read + contents: write + discussions: write + issues: write + pull-requests: write + statuses: read + uses: ./.github/workflows/evergreen.lock.yml + with: + pr_number: ${{ needs.gate.outputs.pr_number }} + head_sha: ${{ needs.gate.outputs.head_sha }} + secrets: inherit diff --git a/.github/workflows/evergreen.lock.yml b/.github/workflows/evergreen.lock.yml index 5523a25f..884c61d2 100644 --- a/.github/workflows/evergreen.lock.yml +++ b/.github/workflows/evergreen.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bdc01a34c314507890520907a0850c0e228c462cafec04a6d035bf2e68b3b27c","body_hash":"3a4e876b6303cad1e6abbc362f4f611fc76b388435aeba963f08d13eb8e3daef","compiler_version":"v0.79.4","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ffc21d646cb24a78a434e2f3665a06ff84e06da1c9c1b43a4c387b51655137f3","body_hash":"3a4e876b6303cad1e6abbc362f4f611fc76b388435aeba963f08d13eb8e3daef","compiler_version":"v0.79.4","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"d059700c6a8ec3b5fd798b9ea60f5d048447b918","version":"v0.79.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.0"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) @@ -80,15 +80,43 @@ name: "Evergreen" on: - pull_request: - types: - - labeled - - synchronize - - reopened - - ready_for_review schedule: - cron: "*/15 * * * *" - # Friendly format: every 15m + workflow_call: + inputs: + aw_context: + default: "" + description: "Agent caller context (used internally by Agentic Workflows)." + required: false + type: string + head_sha: + required: false + type: string + pr_number: + required: true + type: string + outputs: + comment_id: + description: ID of the first added comment + value: ${{ jobs.safe_outputs.outputs.comment_id }} + comment_url: + description: URL of the first added comment + value: ${{ jobs.safe_outputs.outputs.comment_url }} + push_commit_sha: + description: SHA of the pushed commit + value: ${{ jobs.safe_outputs.outputs.push_commit_sha }} + push_commit_url: + description: URL of the pushed commit + value: ${{ jobs.safe_outputs.outputs.push_commit_url }} + secrets: + COPILOT_GITHUB_TOKEN: + required: false + GH_AW_CI_TRIGGER_TOKEN: + required: false + GH_AW_GITHUB_MCP_SERVER_TOKEN: + required: false + GH_AW_GITHUB_TOKEN: + required: false workflow_dispatch: inputs: aw_context: @@ -104,7 +132,7 @@ on: permissions: {} concurrency: - group: evergreen-${{ github.event.pull_request.number || github.event.inputs.pr_number || 'schedule' }} + group: evergreen-${{ inputs.pr_number || github.event.inputs.pr_number || 'schedule' }} queue: max run-name: "Evergreen" @@ -112,9 +140,7 @@ run-name: "Evergreen" jobs: activation: needs: pre_activation - if: > - needs.pre_activation.outputs.activated == 'true' && ((github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'evergreen')) && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id)) + if: needs.pre_activation.outputs.activated == 'true' runs-on: ubuntu-slim permissions: actions: read @@ -122,7 +148,7 @@ jobs: env: GH_AW_MAX_DAILY_AI_CREDITS: "200000" outputs: - body: ${{ steps.sanitized.outputs.body }} + artifact_prefix: ${{ steps.artifact-prefix.outputs.prefix }} comment_id: "" comment_repo: "" daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} @@ -136,8 +162,10 @@ jobs: setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} - text: ${{ steps.sanitized.outputs.text }} - title: ${{ steps.sanitized.outputs.title }} + target_checkout_ref: ${{ steps.resolve-host-repo.outputs.target_checkout_ref }} + target_ref: ${{ steps.resolve-host-repo.outputs.target_ref }} + target_repo: ${{ steps.resolve-host-repo.outputs.target_repo }} + target_repo_name: ${{ steps.resolve-host-repo.outputs.target_repo_name }} steps: - name: Setup Scripts id: setup @@ -155,6 +183,26 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Resolve host repo for activation checkout + id: resolve-host-repo + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + JOB_WORKFLOW_REPOSITORY: ${{ job.workflow_repository }} + JOB_WORKFLOW_SHA: ${{ job.workflow_sha }} + JOB_WORKFLOW_REF: ${{ job.workflow_ref }} + JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + await main(); + - name: Compute artifact prefix + id: artifact-prefix + env: + INPUTS_JSON: ${{ toJSON(inputs) }} + run: bash "${RUNNER_TEMP}/gh-aw/actions/compute_artifact_prefix.sh" - name: Generate agentic run info id: generate_aw_info env: @@ -176,6 +224,7 @@ jobs: GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/evergreen" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_COMPILED_STRICT: "true" + GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -206,10 +255,19 @@ jobs: run: bash "${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: Print cross-repo setup guidance + if: failure() && steps.resolve-host-repo.outputs.target_repo != github.repository + run: | + echo "::error::COPILOT_GITHUB_TOKEN must be configured in the CALLER repository's secrets." + echo "::error::For cross-repo workflow_call, secrets must be set in the repository that triggers the workflow." + echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders + if: steps.resolve-host-repo.outputs.target_repo == github.repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false + repository: ${{ steps.resolve-host-repo.outputs.target_repo }} + ref: ${{ steps.resolve-host-repo.outputs.target_checkout_ref }} sparse-checkout: | .github .agents @@ -250,17 +308,6 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); await main(); - - name: Compute current body text - id: sanitized - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,conda.anaconda.org,conda.binstar.org,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,deb.nodesource.com,deno.land,docs.github.com,esm.sh,files.pythonhosted.org,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.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,patch-diff.githubusercontent.com,pip.pypa.io,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" - with: - script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); - setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); - await main(); - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -439,7 +486,7 @@ jobs: if: success() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: activation + name: ${{ steps.artifact-prefix.outputs.prefix }}activation include-hidden-files: true path: | /tmp/gh-aw/aw_info.json @@ -466,6 +513,9 @@ jobs: issues: read pull-requests: read statuses: read + concurrency: + group: "gh-aw-copilot-evergreen" + queue: max env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -478,6 +528,7 @@ jobs: ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} + artifact_prefix: ${{ needs.activation.outputs.artifact_prefix }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} has_patch: ${{ steps.collect_output.outputs.has_patch }} @@ -506,6 +557,7 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths run: | @@ -536,10 +588,11 @@ jobs: EVENT_PATH: ${{ github.event_path }} GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} - INPUT_PR: ${{ github.event.inputs.pr_number }} + INPUT_HEAD_SHA: ${{ inputs.head_sha }} + INPUT_PR: ${{ inputs.pr_number || github.event.inputs.pr_number }} id: select-pr name: Select Evergreen target PR - run: "python3 - << 'PYEOF'\nimport json\nimport os\nimport urllib.error\nimport urllib.parse\nimport urllib.request\n\ntoken = os.environ[\"GH_TOKEN\"]\nrepo = os.environ[\"GITHUB_REPOSITORY\"]\nevent_name = os.environ.get(\"EVENT_NAME\", \"\")\ninput_pr = (os.environ.get(\"INPUT_PR\") or \"\").strip()\nevent_path = os.environ.get(\"EVENT_PATH\", \"\")\nout_dir = \"/tmp/gh-aw/evergreen\"\nos.makedirs(out_dir, exist_ok=True)\n\ndef api(path):\n req = urllib.request.Request(\n f\"https://api.github.com/repos/{repo}{path}\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n\ndef pr_labels(pr):\n return [label[\"name\"] for label in pr.get(\"labels\", [])]\n\ndef load_event():\n if not event_path or not os.path.exists(event_path):\n return {}\n with open(event_path, encoding=\"utf-8\") as f:\n return json.load(f)\n\ntarget = None\nreason = \"\"\nevent = load_event()\n\ntry:\n if input_pr:\n pr = api(f\"/pulls/{int(input_pr)}\")\n labels = pr_labels(pr)\n if pr.get(\"state\") == \"open\" and \"evergreen\" in labels:\n target = pr\n reason = \"manual dispatch\"\n else:\n reason = \"manual PR is not open with evergreen label\"\n elif event_name == \"pull_request\" and event.get(\"pull_request\"):\n number = event[\"pull_request\"][\"number\"]\n pr = api(f\"/pulls/{number}\")\n labels = pr_labels(pr)\n if pr.get(\"state\") == \"open\" and \"evergreen\" in labels:\n target = pr\n reason = \"pull_request event\"\n else:\n reason = \"pull_request event without evergreen label\"\n else:\n params = urllib.parse.urlencode({\n \"state\": \"open\",\n \"labels\": \"evergreen\",\n \"per_page\": \"50\",\n \"sort\": \"updated\",\n \"direction\": \"asc\",\n })\n issues = api(f\"/issues?{params}\")\n candidates = [issue for issue in issues if \"pull_request\" in issue]\n for issue in candidates:\n labels = [label[\"name\"] for label in issue.get(\"labels\", [])]\n if \"evergreen-exhausted\" not in labels:\n target = api(f\"/pulls/{issue['number']}\")\n reason = \"scheduled oldest labeled PR\"\n break\n if target is None:\n reason = \"no open non-exhausted PR has evergreen label\"\nexcept (ValueError, urllib.error.HTTPError, urllib.error.URLError) as exc:\n reason = f\"target selection failed: {exc}\"\n\npayload = {\n \"selected\": target is not None,\n \"reason\": reason,\n \"event_name\": event_name,\n \"target_pr_number\": target[\"number\"] if target else None,\n \"target_head_sha\": target.get(\"head\", {}).get(\"sha\") if target else None,\n \"target_head_ref\": target.get(\"head\", {}).get(\"ref\") if target else None,\n \"target_base_ref\": target.get(\"base\", {}).get(\"ref\") if target else None,\n}\nwith open(f\"{out_dir}/target.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(payload, f, indent=2)\n\ngh_output = os.environ.get(\"GITHUB_OUTPUT\")\nif gh_output:\n with open(gh_output, \"a\", encoding=\"utf-8\") as f:\n f.write(f\"selected={payload['target_pr_number'] or ''}\\n\")\nprint(json.dumps(payload, indent=2))\nPYEOF\n" + run: "python3 - << 'PYEOF'\nimport json\nimport os\nimport urllib.error\nimport urllib.parse\nimport urllib.request\n\ntoken = os.environ[\"GH_TOKEN\"]\nrepo = os.environ[\"GITHUB_REPOSITORY\"]\nevent_name = os.environ.get(\"EVENT_NAME\", \"\")\ninput_pr = (os.environ.get(\"INPUT_PR\") or \"\").strip()\ninput_head_sha = (os.environ.get(\"INPUT_HEAD_SHA\") or \"\").strip()\nevent_path = os.environ.get(\"EVENT_PATH\", \"\")\nout_dir = \"/tmp/gh-aw/evergreen\"\nos.makedirs(out_dir, exist_ok=True)\n\ndef api(path):\n req = urllib.request.Request(\n f\"https://api.github.com/repos/{repo}{path}\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n\ndef pr_labels(pr):\n return [label[\"name\"] for label in pr.get(\"labels\", [])]\n\ndef load_event():\n if not event_path or not os.path.exists(event_path):\n return {}\n with open(event_path, encoding=\"utf-8\") as f:\n return json.load(f)\n\ntarget = None\nreason = \"\"\nevent = load_event()\n\ntry:\n if input_pr:\n pr = api(f\"/pulls/{int(input_pr)}\")\n labels = pr_labels(pr)\n head_sha = pr.get(\"head\", {}).get(\"sha\")\n if input_head_sha and head_sha != input_head_sha:\n reason = f\"PR head changed from {input_head_sha} to {head_sha}; skipping stale event\"\n elif pr.get(\"state\") == \"open\" and \"evergreen\" in labels:\n target = pr\n reason = \"workflow call\" if event_name == \"workflow_call\" else \"manual dispatch\"\n else:\n reason = \"target PR is not open with evergreen label\"\n else:\n params = urllib.parse.urlencode({\n \"state\": \"open\",\n \"labels\": \"evergreen\",\n \"per_page\": \"50\",\n \"sort\": \"updated\",\n \"direction\": \"asc\",\n })\n issues = api(f\"/issues?{params}\")\n candidates = [issue for issue in issues if \"pull_request\" in issue]\n for issue in candidates:\n labels = [label[\"name\"] for label in issue.get(\"labels\", [])]\n if \"evergreen-exhausted\" not in labels:\n target = api(f\"/pulls/{issue['number']}\")\n reason = \"scheduled oldest labeled PR\"\n break\n if target is None:\n reason = \"no open non-exhausted PR has evergreen label\"\nexcept (ValueError, urllib.error.HTTPError, urllib.error.URLError) as exc:\n reason = f\"target selection failed: {exc}\"\n\npayload = {\n \"selected\": target is not None,\n \"reason\": reason,\n \"event_name\": event_name,\n \"requested_head_sha\": input_head_sha or None,\n \"target_pr_number\": target[\"number\"] if target else None,\n \"target_head_sha\": target.get(\"head\", {}).get(\"sha\") if target else None,\n \"target_head_ref\": target.get(\"head\", {}).get(\"ref\") if target else None,\n \"target_base_ref\": target.get(\"base\", {}).get(\"ref\") if target else None,\n}\nwith open(f\"{out_dir}/target.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(payload, f, indent=2)\n\ngh_output = os.environ.get(\"GITHUB_OUTPUT\")\nif gh_output:\n with open(gh_output, \"a\", encoding=\"utf-8\") as f:\n f.write(f\"selected={payload['target_pr_number'] or ''}\\n\")\nprint(json.dumps(payload, indent=2))\nPYEOF\n" # Repo memory git-based storage configuration from frontmatter processed below - name: Clone repo-memory branch (default) @@ -597,7 +650,7 @@ jobs: - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: activation + name: ${{ needs.activation.outputs.artifact_prefix }}activation path: /tmp/gh-aw - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' @@ -1154,7 +1207,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: repo-memory-default + name: ${{ needs.activation.outputs.artifact_prefix }}repo-memory-default path: /tmp/gh-aw/repo-memory/default retention-days: 1 if-no-files-found: ignore @@ -1163,7 +1216,7 @@ jobs: continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: agent + name: ${{ needs.activation.outputs.artifact_prefix }}agent path: | /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/sandbox/agent/logs/ @@ -1226,12 +1279,13 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + name: ${{ needs.activation.outputs.artifact_prefix }}agent path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1266,7 +1320,7 @@ jobs: continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: usage + name: ${{ needs.activation.outputs.artifact_prefix }}usage path: | /tmp/gh-aw/usage/aw-info.jsonl /tmp/gh-aw/usage/agent_usage.jsonl @@ -1421,12 +1475,13 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + name: ${{ needs.agent.outputs.artifact_prefix }}agent path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1594,7 +1649,7 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: detection + name: ${{ needs.agent.outputs.artifact_prefix }}detection path: /tmp/gh-aw/threat-detection/detection.log if-no-files-found: ignore - name: Parse and conclude threat detection @@ -1631,9 +1686,6 @@ jobs: } pre_activation: - if: > - (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'evergreen')) && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id) runs-on: ubuntu-slim outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} @@ -1655,6 +1707,7 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Check team membership for workflow id: check_membership uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1702,6 +1755,7 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: @@ -1724,7 +1778,7 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 continue-on-error: true with: - name: repo-memory-default + name: ${{ needs.agent.outputs.artifact_prefix }}repo-memory-default path: /tmp/gh-aw/repo-memory/default - name: Push repo-memory changes (default) id: push_repo_memory_default @@ -1806,12 +1860,13 @@ jobs: GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" + GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + name: ${{ needs.activation.outputs.artifact_prefix }}agent path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1824,7 +1879,7 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + name: ${{ needs.activation.outputs.artifact_prefix }}agent path: /tmp/gh-aw/ - name: Extract base branch from agent output id: extract-base-branch @@ -1885,7 +1940,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,conda.anaconda.org,conda.binstar.org,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,deb.nodesource.com,deno.land,docs.github.com,esm.sh,files.pythonhosted.org,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.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,patch-diff.githubusercontent.com,pip.pypa.io,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":false,\"max\":2,\"required_labels\":[\"evergreen\"],\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"evergreen-ready\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\"],\"max\":4,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"CI\":\".yml\"},\"workflows\":[\"CI\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"tests/**\",\"tests-e2e/**\",\"playground/**\",\"golden/**\",\"scripts/**\",\"benchmarks/**\",\"docs/**\"],\"excluded_files\":[\"README.md\",\".autoloop/programs/**\",\".github/workflows/**\",\".github/aw/**\",\"package.json\",\"bun.lock\",\"tsconfig.json\",\"biome.json\",\"bunfig.toml\"],\"if_no_changes\":\"warn\",\"max\":2,\"max_patch_size\":10240,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"bun.lockb\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"required_labels\":[\"evergreen\"],\"signed_commits\":false,\"target\":\"*\"},\"remove_labels\":{\"allowed\":[\"evergreen\",\"evergreen-ready\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\"],\"max\":5,\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":false,\"max\":2,\"required_labels\":[\"evergreen\"],\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"evergreen-ready\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\"],\"max\":4,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"target-ref\":\"${{ needs.activation.outputs.target_ref }}\",\"target-repo\":\"${{ needs.activation.outputs.target_repo }}\",\"workflow_files\":{\"CI\":\".yml\"},\"workflows\":[\"CI\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"tests/**\",\"tests-e2e/**\",\"playground/**\",\"golden/**\",\"scripts/**\",\"benchmarks/**\",\"docs/**\"],\"excluded_files\":[\"README.md\",\".autoloop/programs/**\",\".github/workflows/**\",\".github/aw/**\",\"package.json\",\"bun.lock\",\"tsconfig.json\",\"biome.json\",\"bunfig.toml\"],\"if_no_changes\":\"warn\",\"max\":2,\"max_patch_size\":10240,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"bun.lockb\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"required_labels\":[\"evergreen\"],\"signed_commits\":false,\"target\":\"*\"},\"remove_labels\":{\"allowed\":[\"evergreen\",\"evergreen-ready\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\"],\"max\":5,\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1898,7 +1953,7 @@ jobs: if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: safe-outputs-items + name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json diff --git a/.github/workflows/evergreen.md b/.github/workflows/evergreen.md index 0efe39d7..f219d40d 100644 --- a/.github/workflows/evergreen.md +++ b/.github/workflows/evergreen.md @@ -6,8 +6,14 @@ description: | readiness without directly merging. on: - pull_request: - types: [labeled, synchronize, reopened, ready_for_review] + workflow_call: + inputs: + pr_number: + required: true + type: string + head_sha: + required: false + type: string schedule: every 15m workflow_dispatch: inputs: @@ -16,8 +22,6 @@ on: required: false type: string -if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'evergreen') }} - permissions: contents: read pull-requests: read @@ -42,7 +46,7 @@ checkout: fetch-depth: 0 concurrency: - group: evergreen-${{ github.event.pull_request.number || github.event.inputs.pr_number || 'schedule' }} + group: evergreen-${{ inputs.pr_number || github.event.inputs.pr_number || 'schedule' }} queue: max tools: @@ -152,7 +156,8 @@ steps: GITHUB_REPOSITORY: ${{ github.repository }} EVENT_NAME: ${{ github.event_name }} EVENT_PATH: ${{ github.event_path }} - INPUT_PR: ${{ github.event.inputs.pr_number }} + INPUT_PR: ${{ inputs.pr_number || github.event.inputs.pr_number }} + INPUT_HEAD_SHA: ${{ inputs.head_sha }} run: | python3 - << 'PYEOF' import json @@ -165,6 +170,7 @@ steps: repo = os.environ["GITHUB_REPOSITORY"] event_name = os.environ.get("EVENT_NAME", "") input_pr = (os.environ.get("INPUT_PR") or "").strip() + input_head_sha = (os.environ.get("INPUT_HEAD_SHA") or "").strip() event_path = os.environ.get("EVENT_PATH", "") out_dir = "/tmp/gh-aw/evergreen" os.makedirs(out_dir, exist_ok=True) @@ -198,20 +204,14 @@ steps: if input_pr: pr = api(f"/pulls/{int(input_pr)}") labels = pr_labels(pr) - if pr.get("state") == "open" and "evergreen" in labels: - target = pr - reason = "manual dispatch" - else: - reason = "manual PR is not open with evergreen label" - elif event_name == "pull_request" and event.get("pull_request"): - number = event["pull_request"]["number"] - pr = api(f"/pulls/{number}") - labels = pr_labels(pr) - if pr.get("state") == "open" and "evergreen" in labels: + head_sha = pr.get("head", {}).get("sha") + if input_head_sha and head_sha != input_head_sha: + reason = f"PR head changed from {input_head_sha} to {head_sha}; skipping stale event" + elif pr.get("state") == "open" and "evergreen" in labels: target = pr - reason = "pull_request event" + reason = "workflow call" if event_name == "workflow_call" else "manual dispatch" else: - reason = "pull_request event without evergreen label" + reason = "target PR is not open with evergreen label" else: params = urllib.parse.urlencode({ "state": "open", @@ -237,6 +237,7 @@ steps: "selected": target is not None, "reason": reason, "event_name": event_name, + "requested_head_sha": input_head_sha or None, "target_pr_number": target["number"] if target else None, "target_head_sha": target.get("head", {}).get("sha") if target else None, "target_head_ref": target.get("head", {}).get("ref") if target else None,