From ae5b60e6d7107b78da5faa9e2e4b924997c3f575 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 4 Jun 2026 13:27:57 +0200 Subject: [PATCH 1/3] feat(oblt-aw,docs-aw): replace ingress routing with event orchestrators Remove monolithic ingress entrypoints (#1104) and restore event-scoped orchestrators with consolidated client triggers for #4595. Validation: validate_aw_workflow_prelude.py; pytest tests/ (136 passed). --- .cursor/rules/protected-oblt-aw-workflow.mdc | 8 +- .../docs/.github/workflows/docs-aw.yml | 62 -- .../trigger-docs-aw-issue-comment.yml | 24 + .../workflows/trigger-docs-aw-issues.yml | 29 + .../trigger-docs-aw-pull-request.yml | 20 + .../trigger-docs-aw-workflow-run.yml | 30 + .../.github/workflows/trigger-docs-aw.yml | 83 --- .../obs/.github/workflows/oblt-aw.yml | 63 -- .../trigger-oblt-aw-issue-comment.yml | 20 + .../workflows/trigger-oblt-aw-issues.yml | 22 + .../trigger-oblt-aw-pull-request.yml | 21 + .../workflows/trigger-oblt-aw-schedule.yml | 21 + .../workflows/trigger-oblt-aw-status.yml | 22 + .../obs/.github/workflows/trigger-oblt-aw.yml | 61 -- .github/workflows/aw-prelude.yml | 76 ++- .github/workflows/aw-resolve-apm-assets.yml | 26 +- .github/workflows/ci.yml | 7 +- .github/workflows/docs-aw-ai-menu.yml | 90 +-- .../workflows/docs-aw-event-issue-comment.yml | 70 ++ .github/workflows/docs-aw-event-issues.yml | 46 ++ .../workflows/docs-aw-event-pull-request.yml | 36 ++ .../workflows/docs-aw-event-workflow-run.yml | 46 ++ .github/workflows/docs-aw-ingress.yml | 202 ------ .../workflows/docs-aw-pr-ai-menu-collect.yml | 33 +- .github/workflows/docs-aw-pr-ai-menu.yml | 95 ++- .../workflows/oblt-aw-agent-suggestions.yml | 38 +- .github/workflows/oblt-aw-autodoc.yml | 43 +- .github/workflows/oblt-aw-automerge.yml | 67 +- .../workflows/oblt-aw-dependency-review.yml | 61 +- .../oblt-aw-duplicate-issue-detector.yml | 42 +- .../oblt-aw-estc-pr-buildkite-detective.yml | 45 +- .../workflows/oblt-aw-event-issue-comment.yml | 76 +++ .github/workflows/oblt-aw-event-issues.yml | 173 +++++ .../workflows/oblt-aw-event-pull-request.yml | 72 +++ .github/workflows/oblt-aw-event-schedule.yml | 101 +++ .github/workflows/oblt-aw-event-status.yml | 47 ++ .github/workflows/oblt-aw-ingress.yml | 611 ------------------ .github/workflows/oblt-aw-issue-fixer.yml | 43 +- .github/workflows/oblt-aw-issue-triage.yml | 40 +- .../workflows/oblt-aw-mention-in-issue.yml | 56 +- ...not-accessible-by-integration-detector.yml | 36 +- ...ce-not-accessible-by-integration-fixer.yml | 44 +- ...e-not-accessible-by-integration-triage.yml | 74 ++- .../workflows/oblt-aw-security-detector.yml | 50 +- .github/workflows/oblt-aw-security-fixer.yml | 53 +- .github/workflows/oblt-aw-security-triage.yml | 73 ++- .github/workflows/oblt-aw.yml | 63 -- .../trigger-oblt-aw-agent-suggestions.yml | 18 + .github/workflows/trigger-oblt-aw-autodoc.yml | 19 + .../workflows/trigger-oblt-aw-automerge.yml | 21 + .../trigger-oblt-aw-dependency-review.yml | 20 + ...igger-oblt-aw-duplicate-issue-detector.yml | 19 + ...er-oblt-aw-estc-pr-buildkite-detective.yml | 22 + .../workflows/trigger-oblt-aw-issue-fixer.yml | 19 + .../trigger-oblt-aw-issue-triage.yml | 20 + .../trigger-oblt-aw-mention-in-issue.yml | 20 + ...not-accessible-by-integration-detector.yml | 18 + ...ce-not-accessible-by-integration-fixer.yml | 19 + ...e-not-accessible-by-integration-triage.yml | 21 + .../trigger-oblt-aw-security-detector.yml | 19 + .../trigger-oblt-aw-security-fixer.yml | 20 + .../trigger-oblt-aw-security-triage.yml | 21 + .github/workflows/trigger-oblt-aw.yml | 61 -- AGENTS.md | 11 +- README.md | 4 +- config/docs/active-repositories.json | 5 +- config/docs/workflow-registry.json | 17 +- config/obs/active-repositories.json | 55 +- .../obs/automerge-dependency-collections.json | 15 - config/obs/workflow-registry.json | 89 +-- docs/onboarding/adopting-agentic-workflows.md | 10 +- docs/onboarding/registering-a-repository.md | 11 +- .../control-plane-dashboard-format.md | 2 +- docs/operations/distribute-client-workflow.md | 22 +- docs/workflows/README.md | 3 +- docs/workflows/aw-prelude.md | 20 +- docs/workflows/aw-resolve-apm-assets.md | 30 +- docs/workflows/distribute-client-workflow.md | 2 +- docs/workflows/docs-aw-ai-menu.md | 4 +- docs/workflows/docs-aw-client-template.md | 79 ++- docs/workflows/docs-aw-ingress.md | 33 - docs/workflows/docs-aw-pr-ai-menu.md | 6 +- docs/workflows/oblt-aw-client-template.md | 127 ++-- docs/workflows/oblt-aw-ingress.md | 34 - .../workflows/sync-control-plane-dashboard.md | 2 +- scripts/build_repos_matrix.py | 18 +- scripts/build_target_operations.py | 10 +- scripts/common.py | 61 +- scripts/docs/issue-menu/evaluate-trigger.js | 6 +- scripts/docs/issue-menu/post-menu.js | 6 +- .../refresh-after-docs-issue-scope.js | 3 +- .../issue-menu/refresh-after-docs-triage.js | 3 +- .../docs/issue-menu/refresh-after-trigger.js | 3 +- scripts/docs/lib/relayed-event.js | 31 - scripts/docs/pr-menu/evaluate-trigger.js | 13 +- scripts/docs/pr-menu/post-menu.js | 6 +- .../docs/pr-menu/refresh-after-docs-review.js | 3 +- scripts/docs/pr-menu/refresh-after-trigger.js | 3 +- scripts/evaluate_workflow_gates.py | 135 ++++ scripts/ingress_github_context.py | 240 ------- scripts/oblt_aw_route_specs.py | 521 --------------- scripts/resolve_apm_agentic_assets.py | 19 +- scripts/resolve_control_plane_workflow_id.py | 65 ++ scripts/resolve_repository_token_policy.py | 58 ++ scripts/validate_aw_workflow_prelude.py | 105 ++- scripts/validate_ingress_registry.py | 61 -- scripts/workflow_registry.py | 121 ++-- tests/test_build_repos_matrix.py | 79 ++- tests/test_build_target_operations.py | 19 +- tests/test_evaluate_workflow_gates.py | 86 +++ tests/test_ingress_github_context.py | 103 --- tests/test_oblt_aw_route_specs.py | 339 ---------- tests/test_org_config.py | 28 + tests/test_validate_aw_workflow_prelude.py | 54 +- tests/test_workflow_registry.py | 47 +- ...ssifyAutomergeDependencyCollection.test.ts | 37 -- 116 files changed, 2621 insertions(+), 3721 deletions(-) delete mode 100644 .github/remote-workflow-template/docs/.github/workflows/docs-aw.yml create mode 100644 .github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issue-comment.yml create mode 100644 .github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issues.yml create mode 100644 .github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pull-request.yml create mode 100644 .github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-workflow-run.yml delete mode 100644 .github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw.yml delete mode 100644 .github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml create mode 100644 .github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-comment.yml create mode 100644 .github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issues.yml create mode 100644 .github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-pull-request.yml create mode 100644 .github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-schedule.yml create mode 100644 .github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-status.yml delete mode 100644 .github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml create mode 100644 .github/workflows/docs-aw-event-issue-comment.yml create mode 100644 .github/workflows/docs-aw-event-issues.yml create mode 100644 .github/workflows/docs-aw-event-pull-request.yml create mode 100644 .github/workflows/docs-aw-event-workflow-run.yml delete mode 100644 .github/workflows/docs-aw-ingress.yml create mode 100644 .github/workflows/oblt-aw-event-issue-comment.yml create mode 100644 .github/workflows/oblt-aw-event-issues.yml create mode 100644 .github/workflows/oblt-aw-event-pull-request.yml create mode 100644 .github/workflows/oblt-aw-event-schedule.yml create mode 100644 .github/workflows/oblt-aw-event-status.yml delete mode 100644 .github/workflows/oblt-aw-ingress.yml delete mode 100644 .github/workflows/oblt-aw.yml create mode 100644 .github/workflows/trigger-oblt-aw-agent-suggestions.yml create mode 100644 .github/workflows/trigger-oblt-aw-autodoc.yml create mode 100644 .github/workflows/trigger-oblt-aw-automerge.yml create mode 100644 .github/workflows/trigger-oblt-aw-dependency-review.yml create mode 100644 .github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml create mode 100644 .github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml create mode 100644 .github/workflows/trigger-oblt-aw-issue-fixer.yml create mode 100644 .github/workflows/trigger-oblt-aw-issue-triage.yml create mode 100644 .github/workflows/trigger-oblt-aw-mention-in-issue.yml create mode 100644 .github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml create mode 100644 .github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml create mode 100644 .github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml create mode 100644 .github/workflows/trigger-oblt-aw-security-detector.yml create mode 100644 .github/workflows/trigger-oblt-aw-security-fixer.yml create mode 100644 .github/workflows/trigger-oblt-aw-security-triage.yml delete mode 100644 .github/workflows/trigger-oblt-aw.yml delete mode 100644 docs/workflows/docs-aw-ingress.md delete mode 100644 docs/workflows/oblt-aw-ingress.md delete mode 100644 scripts/docs/lib/relayed-event.js create mode 100644 scripts/evaluate_workflow_gates.py delete mode 100644 scripts/ingress_github_context.py delete mode 100644 scripts/oblt_aw_route_specs.py create mode 100644 scripts/resolve_control_plane_workflow_id.py create mode 100644 scripts/resolve_repository_token_policy.py delete mode 100644 scripts/validate_ingress_registry.py create mode 100644 tests/test_evaluate_workflow_gates.py delete mode 100644 tests/test_ingress_github_context.py delete mode 100644 tests/test_oblt_aw_route_specs.py diff --git a/.cursor/rules/protected-oblt-aw-workflow.mdc b/.cursor/rules/protected-oblt-aw-workflow.mdc index cc7e92d6..b2f8aaac 100644 --- a/.cursor/rules/protected-oblt-aw-workflow.mdc +++ b/.cursor/rules/protected-oblt-aw-workflow.mdc @@ -1,14 +1,14 @@ --- -description: Client workflows — `.github/remote-workflow-template/obs/` +description: Client workflow templates live under remote-workflow-template only alwaysApply: true --- # Client workflows — `.github/remote-workflow-template/obs/` -**Do not** add per-workflow `trigger-oblt-aw-*.yml` client entrypoints. +**Do not** add a monolithic `.github/workflows/oblt-aw.yml` entrypoint (removed). -**Where to change consumer entrypoints:** [`.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml`](.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml) and [`.github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml`](.github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml) only. Distribution installs that tree into target repositories. +**Where to change consumer entrypoints:** [`.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-*.yml`](.github/remote-workflow-template/obs/.github/workflows/) only. Distribution installs that tree into target repositories. -**Control-plane reusables** live under `.github/workflows/oblt-aw-*.yml` (ingress: `oblt-aw-ingress.yml`; shared prelude: `aw-prelude.yml`). **Do not** edit consumer copies under target repos from this repository except via distribution. +**Control-plane reusables** live under `.github/workflows/oblt-aw-*.yml` and `.github/workflows/docs-aw-*.yml` (shared prelude: `aw-prelude.yml`). **Do not** edit consumer copies under target repos from this repository except via distribution. **Docs:** [docs/workflows/oblt-aw-client-template.md](docs/workflows/oblt-aw-client-template.md), [CONTRIBUTING.md](CONTRIBUTING.md). diff --git a/.github/remote-workflow-template/docs/.github/workflows/docs-aw.yml b/.github/remote-workflow-template/docs/.github/workflows/docs-aw.yml deleted file mode 100644 index f836db87..00000000 --- a/.github/remote-workflow-template/docs/.github/workflows/docs-aw.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Docs Agentic Workflow Entrypoint - -on: - workflow_dispatch: - inputs: - trigger-source: - description: Client trigger workflow file that dispatched this run - required: true - type: string - event-name: - description: Original github.event_name from the trigger - required: true - type: string - event-action: - description: Original github.event.action from the trigger - required: false - type: string - default: '' - event-payload-json: - description: JSON-encoded original github.event from the trigger - required: true - type: string - caller-ref: - description: Original github.ref from the trigger - required: true - type: string - caller-sha: - description: Original github.sha from the trigger - required: true - type: string - caller-run-id: - description: Original github.run_id from the trigger - required: true - type: string - -permissions: - contents: read - -concurrency: - group: docs-aw-entrypoint-${{ github.event.inputs.event-name }}-${{ github.event.inputs.caller-run-id }} - cancel-in-progress: true - -jobs: - ingress: - permissions: - actions: write - checks: read - contents: read - discussions: write - issues: write - pull-requests: write - uses: elastic/oblt-aw/.github/workflows/docs-aw-ingress.yml@main # ratchet:exclude - with: - trigger-source: ${{ github.event.inputs.trigger-source }} - ingress-event-name: ${{ github.event.inputs.event-name }} - ingress-event-action: ${{ github.event.inputs.event-action }} - ingress-event-payload-json: ${{ github.event.inputs.event-payload-json }} - caller-ref: ${{ github.event.inputs.caller-ref }} - caller-sha: ${{ github.event.inputs.caller-sha }} - caller-run-id: ${{ github.event.inputs.caller-run-id }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issue-comment.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issue-comment.yml new file mode 100644 index 00000000..6f79c446 --- /dev/null +++ b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issue-comment.yml @@ -0,0 +1,24 @@ +name: Docs Agentic Workflow — Issue Comment + +# Distributed to repositories in config/docs/active-repositories.json. + +on: + issue_comment: + types: [edited] + +permissions: + contents: read + issues: read + +jobs: + run-aw: + permissions: + actions: read + checks: read + contents: read + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/docs-aw-event-issue-comment.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issues.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issues.yml new file mode 100644 index 00000000..c5841733 --- /dev/null +++ b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-issues.yml @@ -0,0 +1,29 @@ +name: Docs Agentic Workflow — Issues + +# Distributed to repositories in config/docs/active-repositories.json. + +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: Issue number for Docs AI issue menu (manual refresh). + required: true + type: string + +permissions: + contents: read + issues: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/docs-aw-event-issues.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pull-request.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pull-request.yml new file mode 100644 index 00000000..88e9d9ca --- /dev/null +++ b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pull-request.yml @@ -0,0 +1,20 @@ +name: Docs Agentic Workflow — AI PR Menu (collect) + +# Distributed to repositories in config/docs/active-repositories.json. +# Fork-safe `pull_request` leg of the split-workflow pattern (see trigger-docs-aw-workflow-run.yml). + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + +permissions: + contents: read + issues: read + +jobs: + run-aw: + permissions: + actions: write + contents: read + issues: read + uses: elastic/oblt-aw/.github/workflows/docs-aw-event-pull-request.yml@main # ratchet:exclude diff --git a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-workflow-run.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-workflow-run.yml new file mode 100644 index 00000000..bd6d8634 --- /dev/null +++ b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-workflow-run.yml @@ -0,0 +1,30 @@ +name: Docs Agentic Workflow — AI PR Menu + +# Distributed to repositories in config/docs/active-repositories.json. + +on: + workflow_run: + workflows: ["Docs Agentic Workflow — AI PR Menu (collect)"] + types: [completed] + workflow_dispatch: + inputs: + pull_request_number: + description: Pull request number for Docs PR AI menu (manual refresh). + required: true + type: string + +permissions: + contents: read + issues: read + +jobs: + run-aw: + permissions: + actions: read + checks: read + contents: read + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/docs-aw-event-workflow-run.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw.yml deleted file mode 100644 index 84fc3a02..00000000 --- a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Docs Agentic Workflow Trigger - -# Distributed to repositories in config/docs/active-repositories.json. - -on: - issues: - types: [opened] - issue_comment: - types: [edited] - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - workflow_run: - workflows: [Docs Agentic Workflow Trigger] - types: [completed] - workflow_dispatch: - inputs: - issue_number: - description: Issue number for Docs AI issue menu (manual refresh). - required: false - type: string - pull_request_number: - description: Pull request number for Docs PR AI menu (manual refresh). - required: false - type: string - -permissions: - contents: read - -concurrency: - group: docs-aw-trigger-${{ github.event.pull_request.number || github.event.issue.number || github.event.workflow_run.id || github.event.schedule || github.sha }} - cancel-in-progress: true - -jobs: - dispatch-entrypoint: - if: >- - github.event_name != 'workflow_run' || - ( - github.event.workflow_run.conclusion == 'success' && - github.event.workflow_run.event == 'pull_request' - ) - permissions: - actions: write - statuses: write - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Dispatch docs-aw entrypoint - id: dispatch - uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - workflow: docs-aw.yml - inputs: | - { - "trigger-source": ${{ toJSON(github.workflow) }}, - "event-name": ${{ toJSON(github.event_name) }}, - "event-action": ${{ toJSON(github.event.action || '') }}, - "event-payload-json": ${{ toJSON(toJSON(github.event)) }}, - "caller-ref": ${{ toJSON(github.ref) }}, - "caller-sha": ${{ toJSON(github.sha) }}, - "caller-run-id": ${{ toJSON(github.run_id) }} - } - - - name: Post entrypoint run link on PR commit - if: >- - github.event_name == 'pull_request' || - (github.event_name == 'workflow_run' && - github.event.workflow_run.event == 'pull_request') - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HEAD_SHA: >- - ${{ github.event.pull_request.head.sha || - github.event.workflow_run.head_sha }} - RUN_URL: ${{ steps.dispatch.outputs.runUrlHtml }} - STATUS_CONTEXT: Documentation Agentic Workflow Execution - run: | - gh api \ - --method POST \ - "/repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \ - -f state=success \ - -f target_url="${RUN_URL}" \ - -f description='Documentation agentic workflow entrypoint dispatched (open run for progress)' \ - -f context="${STATUS_CONTEXT}" diff --git a/.github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml b/.github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml deleted file mode 100644 index b70c33ed..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Observability Agentic Workflow Entrypoint - -on: - workflow_dispatch: - inputs: - trigger-source: - description: Client trigger workflow file that dispatched this run - required: true - type: string - event-name: - description: Original github.event_name from the trigger - required: true - type: string - event-action: - description: Original github.event.action from the trigger - required: false - type: string - default: '' - event-payload-json: - description: JSON-encoded original github.event from the trigger - required: true - type: string - caller-ref: - description: Original github.ref from the trigger - required: true - type: string - caller-sha: - description: Original github.sha from the trigger - required: true - type: string - caller-run-id: - description: Original github.run_id from the trigger - required: true - type: string - -permissions: - contents: read - -concurrency: - group: oblt-aw-entrypoint-${{ github.event.inputs.event-name }}-${{ github.event.inputs.caller-run-id }} - cancel-in-progress: true - -jobs: - ingress: - permissions: - actions: read - contents: write - discussions: write - id-token: write - issues: write - pull-requests: write - uses: elastic/oblt-aw/.github/workflows/oblt-aw-ingress.yml@main # ratchet:exclude - with: - trigger-source: ${{ github.event.inputs.trigger-source }} - ingress-event-name: ${{ github.event.inputs.event-name }} - ingress-event-action: ${{ github.event.inputs.event-action }} - ingress-event-payload-json: ${{ github.event.inputs.event-payload-json }} - caller-ref: ${{ github.event.inputs.caller-ref }} - caller-sha: ${{ github.event.inputs.caller-sha }} - caller-run-id: ${{ github.event.inputs.caller-run-id }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_LOGS_API_TOKEN }} diff --git a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-comment.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-comment.yml new file mode 100644 index 00000000..e154f930 --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-comment.yml @@ -0,0 +1,20 @@ +name: Observability Agentic Workflow — Issue Comment + +on: + issue_comment: + types: [created] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-event-issue-comment.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issues.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issues.yml new file mode 100644 index 00000000..3e037b7c --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issues.yml @@ -0,0 +1,22 @@ +name: Observability Agentic Workflow — Issues + +on: + issues: + types: [opened, labeled] + workflow_dispatch: + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-event-issues.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-pull-request.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-pull-request.yml new file mode 100644 index 00000000..08cc7234 --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-pull-request.yml @@ -0,0 +1,21 @@ +name: Observability Agentic Workflow — Pull Request + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-event-pull-request.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-schedule.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-schedule.yml new file mode 100644 index 00000000..464feade --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-schedule.yml @@ -0,0 +1,21 @@ +name: Observability Agentic Workflow — Schedule + +on: + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-event-schedule.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-status.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-status.yml new file mode 100644 index 00000000..b4be1c5c --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-status.yml @@ -0,0 +1,22 @@ +name: Observability Agentic Workflow — Status + +on: + status: + +permissions: + contents: read + +jobs: + run-aw: + if: >- + github.event.state == 'failure' && + contains(github.event.context, 'buildkite') + permissions: + actions: read + contents: read + issues: read + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-event-status.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_LOGS_API_TOKEN }} diff --git a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml deleted file mode 100644 index 12564382..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Observability Agentic Workflow Trigger - -on: - schedule: - - cron: "0 6 * * *" - workflow_dispatch: - issues: - types: [opened, labeled] - issue_comment: - types: [created] - pull_request: - types: [opened, synchronize, reopened, labeled] - status: - -permissions: - contents: read - -concurrency: - group: oblt-aw-trigger-${{ github.event.pull_request.number || github.event.issue.number || github.event.schedule || github.sha }} - cancel-in-progress: true - -jobs: - dispatch-entrypoint: - permissions: - actions: write - statuses: write - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Dispatch oblt-aw entrypoint - id: dispatch - uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - workflow: oblt-aw.yml - inputs: | - { - "trigger-source": ${{ toJSON(github.workflow) }}, - "event-name": ${{ toJSON(github.event_name) }}, - "event-action": ${{ toJSON(github.event.action || '') }}, - "event-payload-json": ${{ toJSON(toJSON(github.event)) }}, - "caller-ref": ${{ toJSON(github.ref) }}, - "caller-sha": ${{ toJSON(github.sha) }}, - "caller-run-id": ${{ toJSON(github.run_id) }} - } - - - name: Post entrypoint run link on PR commit - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - RUN_URL: ${{ steps.dispatch.outputs.runUrlHtml }} - STATUS_CONTEXT: Observability Agentic Workflow Execution - run: | - gh api \ - --method POST \ - "/repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \ - -f state=success \ - -f target_url="${RUN_URL}" \ - -f description='Observability agentic workflow entrypoint dispatched (open run for progress)' \ - -f context="${STATUS_CONTEXT}" diff --git a/.github/workflows/aw-prelude.yml b/.github/workflows/aw-prelude.yml index a0854b2f..65ed89fd 100644 --- a/.github/workflows/aw-prelude.yml +++ b/.github/workflows/aw-prelude.yml @@ -1,24 +1,27 @@ name: Agentic Workflow Prelude -# Shared dashboard read and optional allow-list loading for ingress workflows. -# APM asset resolution lives in aw-resolve-apm-assets.yml (per gh-aw-* invocation). +# Shared dashboard read, optional allow-list loading, and per-route proceed flags +# for event-scoped orchestrators (one prelude chain per GitHub event family). +# APM asset resolution lives in aw-resolve-apm-assets.yml (per gh-aw-* invocation in route reusables). on: workflow_call: inputs: + control-plane-workflows: + description: >- + JSON array of control-plane workflow basenames (for example + ["oblt-aw-automerge.yml","oblt-aw-dependency-review.yml"]). + required: true + type: string load-allowed-authors: description: Load PR and issue allow lists from elastic/oblt-aw config required: false type: boolean default: false - ingress-event-name: - description: Relayed event name from ingress (empty uses github.event_name) - required: false - type: string - default: '' outputs: - proceed: - description: Always true; route jobs gate on enabled-workflows in ingress - value: ${{ jobs.evaluate.outputs.proceed }} + proceed-by-workflow: + description: >- + JSON map of control-plane workflow basename to proceed (true/false). + value: ${{ jobs.evaluate.outputs.proceed-by-workflow }} effective-raw: description: Raw dashboard read before normalization ('' means all enabled) value: ${{ jobs.dashboard.outputs.effective-raw }} @@ -37,6 +40,11 @@ on: allowed-issue-authors-csv: description: Comma-separated allowed issue bot logins (empty when not loaded) value: ${{ jobs.evaluate.outputs.allowed-issue-authors-csv }} + token-policy: + description: >- + Token policy from config//active-repositories.json for this repository + when set; empty when not configured (create-token uses Vault auto policy). + value: ${{ jobs.evaluate.outputs.token-policy }} permissions: contents: read @@ -53,10 +61,7 @@ jobs: contents: read if: >- inputs.load-allowed-authors && - ( - (inputs.ingress-event-name != '' && inputs.ingress-event-name || github.event_name) == 'pull_request' || - (inputs.ingress-event-name != '' && inputs.ingress-event-name || github.event_name) == 'issues' - ) + (github.event_name == 'pull_request' || github.event_name == 'issues') uses: ./.github/workflows/load-allowed-authors.yml evaluate: @@ -70,15 +75,50 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 2 outputs: - proceed: ${{ steps.gate.outputs.proceed }} + proceed-by-workflow: ${{ steps.gates.outputs.proceed-by-workflow }} allowed-pr-authors-json: ${{ steps.pack.outputs.allowed-pr-authors-json }} allowed-pr-authors-csv: ${{ steps.pack.outputs.allowed-pr-authors-csv }} allowed-issue-authors-json: ${{ steps.pack.outputs.allowed-issue-authors-json }} allowed-issue-authors-csv: ${{ steps.pack.outputs.allowed-issue-authors-csv }} + token-policy: ${{ steps.token-policy.outputs.token-policy }} steps: - - name: Set proceed output - id: gate - run: echo "proceed=true" >> "${GITHUB_OUTPUT}" + - name: Checkout oblt-aw (registry scripts + config) + uses: actions/checkout@v6 + with: + repository: elastic/oblt-aw + ref: main + path: _oblt-aw + fetch-depth: 1 + token: ${{ github.token }} + sparse-checkout: | + scripts/evaluate_workflow_gates.py + scripts/workflow_registry.py + scripts/common.py + scripts/resolve_repository_token_policy.py + config/ + sparse-checkout-cone-mode: false + + - name: Resolve repository token policy + id: token-policy + env: + CONFIG_DIR: _oblt-aw/config + TARGET_REPOSITORY: ${{ github.repository }} + run: python _oblt-aw/scripts/resolve_repository_token_policy.py + + - name: Evaluate dashboard gates for event routes + id: gates + env: + CONFIG_DIR: _oblt-aw/config + CONTROL_PLANE_WORKFLOWS: ${{ inputs.control-plane-workflows }} + EFFECTIVE_RAW: ${{ needs.dashboard.outputs.effective-raw }} + ENABLED_WORKFLOWS: ${{ needs.dashboard.outputs.enabled-workflows }} + run: | + set -euo pipefail + python _oblt-aw/scripts/evaluate_workflow_gates.py \ + --config-dir "${CONFIG_DIR}" \ + --control-plane-workflows "${CONTROL_PLANE_WORKFLOWS}" \ + --effective-raw "${EFFECTIVE_RAW}" \ + --enabled-workflows "${ENABLED_WORKFLOWS}" - name: Pack allow-list outputs id: pack diff --git a/.github/workflows/aw-resolve-apm-assets.yml b/.github/workflows/aw-resolve-apm-assets.yml index 11dd11f7..4d53b316 100644 --- a/.github/workflows/aw-resolve-apm-assets.yml +++ b/.github/workflows/aw-resolve-apm-assets.yml @@ -30,21 +30,6 @@ on: required: false type: boolean default: true - ingress-event-name: - description: Relayed github.event_name from ingress (injected into gh-aw prompt context) - required: false - type: string - default: "" - ingress-event-action: - description: Relayed github.event.action from ingress (injected into gh-aw prompt context) - required: false - type: string - default: "" - ingress-event-payload-json: - description: Relayed github.event JSON from ingress (injected into gh-aw prompt context) - required: false - type: string - default: "" outputs: apm-manifest-present: description: True when the consumer repository contains apm.yml or apm.yaml @@ -95,10 +80,9 @@ jobs: token: ${{ github.token }} sparse-checkout: | scripts/apm_agentic_assets.py - scripts/ingress_github_context.py scripts/resolve_apm_agentic_assets.py + scripts/resolve_control_plane_workflow_id.py scripts/workflow_registry.py - scripts/oblt_aw_route_specs.py scripts/common.py config/ requirements-runtime.txt @@ -108,10 +92,7 @@ jobs: id: registry env: CONTROL_PLANE_WORKFLOW: ${{ inputs.control-plane-workflow }} - run: >- - python _oblt-aw/scripts/workflow_registry.py - "${CONTROL_PLANE_WORKFLOW}" - --config-dir _oblt-aw/config + run: python _oblt-aw/scripts/resolve_control_plane_workflow_id.py "${CONTROL_PLANE_WORKFLOW}" --config-dir _oblt-aw/config - name: Detect apm manifest id: detect @@ -175,7 +156,4 @@ jobs: CONTROL_PLANE_CONFIG_DIR: ${{ github.workspace }}/_oblt-aw/config PLATFORM_ADDITIONAL_INSTRUCTIONS: ${{ inputs.platform-additional-instructions }} PLATFORM_INPUTS_JSON: ${{ inputs.platform-inputs-json }} - INGRESS_EVENT_NAME: ${{ inputs.ingress-event-name }} - INGRESS_EVENT_ACTION: ${{ inputs.ingress-event-action }} - INGRESS_EVENT_PAYLOAD_JSON: ${{ inputs.ingress-event-payload-json }} run: python _oblt-aw/scripts/resolve_apm_agentic_assets.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfb55962..4470ade1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,9 @@ jobs: with: python-version: "3.14" cache: pip - cache-dependency-path: requirements-ci.txt + cache-dependency-path: | + requirements-ci.txt + requirements-runtime.txt - name: Install Python test dependencies run: pip install -r requirements-ci.txt @@ -56,9 +58,6 @@ jobs: - name: Validate *-aw-* workflows call aw-prelude run: python scripts/validate_aw_workflow_prelude.py - - name: Validate ingress route registries - run: python scripts/validate_ingress_registry.py - - name: Validate gh-aw-* workflows call resolve-apm-assets run: python scripts/validate_aw_workflow_resolve_apm_assets.py diff --git a/.github/workflows/docs-aw-ai-menu.yml b/.github/workflows/docs-aw-ai-menu.yml index 819e9fbc..58119434 100644 --- a/.github/workflows/docs-aw-ai-menu.yml +++ b/.github/workflows/docs-aw-ai-menu.yml @@ -1,23 +1,29 @@ name: Docs AI menu -# Reusable implementation for the Elastic Docs issue AI menu. Routed from docs-aw-ingress.yml. +# Reusable implementation for the Elastic Docs issue AI menu. Consumer repositories +# install event-scoped client templates under `.github/remote-workflow-template/docs/.github/workflows/`. # Scripts: `scripts/docs/issue-menu/`. on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from docs-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from docs-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true + type: string + shared-allowed-pr-authors-csv: + required: true + type: string + shared-allowed-issue-authors-json: + required: true + type: string + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from docs-aw-ingress + shared-token-policy: required: true type: string secrets: @@ -31,21 +37,15 @@ jobs: post-menu: name: Post or refresh AI menu if: >- - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'issues' || - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'workflow_dispatch' + inputs.shared-proceed == 'true' && + (github.event_name == 'issues' || github.event_name == 'workflow_dispatch') runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read issues: write concurrency: - group: docs-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.issue_number }} + group: docs-ai-menu-${{ github.event.issue.number || github.event.inputs.issue_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs AI issue menu scripts @@ -56,14 +56,11 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/issue-menu sparse-checkout-cone-mode: true - name: Create or update AI menu comment uses: actions/github-script@v9 - env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -73,15 +70,13 @@ jobs: evaluate-trigger: name: Evaluate AI menu trigger if: >- - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'issue_comment' && - fromJSON(inputs.ingress-event-payload-json).issue.pull_request == null && + inputs.shared-proceed == 'true' && + github.event_name == 'issue_comment' && + github.event.issue.pull_request == null && github.actor != 'github-actions[bot]' && - fromJSON(inputs.ingress-event-payload-json).comment.user.login == 'github-actions[bot]' && - contains(fromJSON(inputs.ingress-event-payload-json).comment.body, '') && - contains(fromJSON(inputs.ingress-event-payload-json).comment.body, '') + github.event.comment.user.login == 'github-actions[bot]' && + contains(github.event.comment.body, '') && + contains(github.event.comment.body, '') runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -98,15 +93,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/issue-menu sparse-checkout-cone-mode: true - name: Parse AI menu transition id: evaluate uses: actions/github-script@v9 - env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -125,7 +117,7 @@ jobs: contents: read issues: write concurrency: - group: docs-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.issue_number }} + group: docs-ai-menu-${{ github.event.issue.number || github.event.inputs.issue_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs AI issue menu scripts @@ -136,14 +128,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/issue-menu sparse-checkout-cone-mode: true - name: Refresh AI menu status uses: actions/github-script@v9 env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} TRIAGE_TRIGGERED: ${{ needs.evaluate-trigger.outputs.triage_triggered }} ISSUE_SCOPE_TRIGGERED: ${{ needs.evaluate-trigger.outputs.issue_scope_triggered }} with: @@ -154,19 +144,18 @@ jobs: resolve-apm-assets-triage: needs: [evaluate-trigger] - permissions: - contents: read + if: >- + inputs.shared-proceed == 'true' && + needs.evaluate-trigger.outputs.triage_triggered == 'true' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: docs-aw-ai-menu.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} run-docs-triage: name: Docs AI / triage needs: [evaluate-trigger, resolve-apm-assets-triage] if: >- + inputs.shared-proceed == 'true' && needs.evaluate-trigger.outputs.triage_triggered == 'true' permissions: actions: read @@ -176,27 +165,23 @@ jobs: uses: elastic/docs-actions/.github/workflows/gh-aw-issue-triage.lock.yml@v1 with: additional-instructions: ${{ needs.resolve-apm-assets-triage.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets-triage.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} resolve-apm-assets-issue-scope: needs: [evaluate-trigger] if: >- + inputs.shared-proceed == 'true' && needs.evaluate-trigger.outputs.issue_scope_triggered == 'true' - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: docs-aw-ai-menu.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} run-docs-issue-scope: name: Docs AI / issue scope needs: [evaluate-trigger, resolve-apm-assets-issue-scope] if: >- + inputs.shared-proceed == 'true' && needs.evaluate-trigger.outputs.issue_scope_triggered == 'true' permissions: actions: read @@ -207,7 +192,6 @@ jobs: uses: elastic/docs-actions/.github/workflows/gh-aw-docs-issue-scope.lock.yml@v1 with: additional-instructions: ${{ needs.resolve-apm-assets-issue-scope.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets-issue-scope.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -221,7 +205,7 @@ jobs: contents: read issues: write concurrency: - group: docs-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.issue_number }} + group: docs-ai-menu-${{ github.event.issue.number || github.event.inputs.issue_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs AI issue menu scripts @@ -232,14 +216,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/issue-menu sparse-checkout-cone-mode: true - name: Refresh AI menu status uses: actions/github-script@v9 env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} TRIAGE_RESULT: ${{ needs.run-docs-triage.result }} with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -257,7 +239,7 @@ jobs: contents: read issues: write concurrency: - group: docs-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.issue_number }} + group: docs-ai-menu-${{ github.event.issue.number || github.event.inputs.issue_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs AI issue menu scripts @@ -268,14 +250,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/issue-menu sparse-checkout-cone-mode: true - name: Refresh AI menu status uses: actions/github-script@v9 env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} ISSUE_SCOPE_RESULT: ${{ needs.run-docs-issue-scope.result }} with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs-aw-event-issue-comment.yml b/.github/workflows/docs-aw-event-issue-comment.yml new file mode 100644 index 00000000..0ac65318 --- /dev/null +++ b/.github/workflows/docs-aw-event-issue-comment.yml @@ -0,0 +1,70 @@ +name: Docs Agentic Workflow — Issue Comment Event + +# Event-scoped orchestrator: one shared prelude chain for issue_comment routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: false + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: >- + ["docs-aw-ai-menu.yml","docs-aw-pr-ai-menu.yml"] + load-allowed-authors: false + + ai-menu: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-ai-menu.yml'] == 'true' && + github.event_name == 'issue_comment' && + github.event.action == 'edited' && + github.event.issue.pull_request == null + permissions: + actions: read + contents: read + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/docs-aw-ai-menu.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-ai-menu.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + pr-ai-menu: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-pr-ai-menu.yml'] == 'true' && + github.event_name == 'issue_comment' && + github.event.action == 'edited' && + github.event.issue.pull_request != null + permissions: + actions: read + checks: read + contents: read + issues: write + pull-requests: write + uses: ./.github/workflows/docs-aw-pr-ai-menu.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-pr-ai-menu.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/docs-aw-event-issues.yml b/.github/workflows/docs-aw-event-issues.yml new file mode 100644 index 00000000..5706b82e --- /dev/null +++ b/.github/workflows/docs-aw-event-issues.yml @@ -0,0 +1,46 @@ +name: Docs Agentic Workflow — Issues Event + +# Event-scoped orchestrator: one shared prelude chain for issues/workflow_dispatch routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: false + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: '["docs-aw-ai-menu.yml"]' + load-allowed-authors: false + + ai-menu: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-ai-menu.yml'] == 'true' && + ( + (github.event_name == 'issues' && github.event.action == 'opened') || + github.event_name == 'workflow_dispatch' + ) + permissions: + actions: read + contents: read + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/docs-aw-ai-menu.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-ai-menu.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/docs-aw-event-pull-request.yml b/.github/workflows/docs-aw-event-pull-request.yml new file mode 100644 index 00000000..b8de4492 --- /dev/null +++ b/.github/workflows/docs-aw-event-pull-request.yml @@ -0,0 +1,36 @@ +name: Docs Agentic Workflow — Pull Request Event + +# Event-scoped orchestrator: fork-safe PR number collection for the Docs PR AI menu. +on: + workflow_call: + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: '["docs-aw-pr-ai-menu-collect.yml"]' + load-allowed-authors: true + + pr-ai-menu-collect: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-pr-ai-menu-collect.yml'] == 'true' && + github.event_name == 'pull_request' && + contains(fromJSON('["opened","reopened","synchronize","ready_for_review"]'), github.event.action) + permissions: + actions: write + contents: read + uses: ./.github/workflows/docs-aw-pr-ai-menu-collect.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-pr-ai-menu-collect.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} diff --git a/.github/workflows/docs-aw-event-workflow-run.yml b/.github/workflows/docs-aw-event-workflow-run.yml new file mode 100644 index 00000000..9646286f --- /dev/null +++ b/.github/workflows/docs-aw-event-workflow-run.yml @@ -0,0 +1,46 @@ +name: Docs Agentic Workflow — Workflow Run Event + +# Event-scoped orchestrator: privileged PR AI menu post after collect workflow_run. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: false + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: '["docs-aw-pr-ai-menu.yml"]' + load-allowed-authors: false + + pr-ai-menu: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-pr-ai-menu.yml'] == 'true' && + ( + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + github.event_name == 'workflow_dispatch' + ) + permissions: + actions: read + checks: read + contents: read + issues: write + pull-requests: write + uses: ./.github/workflows/docs-aw-pr-ai-menu.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['docs-aw-pr-ai-menu.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/docs-aw-ingress.yml b/.github/workflows/docs-aw-ingress.yml deleted file mode 100644 index 2b6a2a6a..00000000 --- a/.github/workflows/docs-aw-ingress.yml +++ /dev/null @@ -1,202 +0,0 @@ -name: Docs Agentic Workflow Ingress - -# Dynamic routing: each route-* job carries its own if: gate; only eligible workflows run. -# Event context is relayed from trigger-docs-aw.yml via docs-aw.yml workflow_dispatch inputs. -on: - workflow_call: - inputs: - trigger-source: - description: Client trigger workflow that dispatched the entrypoint - required: true - type: string - ingress-event-name: - description: Original github.event_name from the client trigger - required: true - type: string - ingress-event-action: - description: Original github.event.action from the client trigger - required: false - type: string - default: '' - ingress-event-payload-json: - description: JSON-encoded original github.event from the client trigger - required: true - type: string - caller-ref: - description: Original github.ref from the client trigger - required: true - type: string - caller-sha: - description: Original github.sha from the client trigger - required: true - type: string - caller-run-id: - description: Original github.run_id from the client trigger - required: true - type: string - secrets: - COPILOT_GITHUB_TOKEN: - required: false - -permissions: - contents: read - -jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - load-allowed-authors: false - - route-ai-menu: - needs: prelude - permissions: - actions: read - contents: read - discussions: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - ( - ( - inputs.ingress-event-name == 'issues' && - inputs.ingress-event-action == 'opened' - ) || - ( - inputs.ingress-event-name == 'issue_comment' && - inputs.ingress-event-action == 'edited' && - fromJSON(inputs.ingress-event-payload-json).issue.pull_request == null - ) || - inputs.ingress-event-name == 'workflow_dispatch' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'docs:docs-issue-ai-menu' - ) - ) - uses: ./.github/workflows/docs-aw-ai-menu.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-pr-ai-menu-collect: - needs: prelude - permissions: - actions: write - contents: read - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'pull_request' && - ( - inputs.ingress-event-action == 'opened' || - inputs.ingress-event-action == 'reopened' || - inputs.ingress-event-action == 'synchronize' || - inputs.ingress-event-action == 'ready_for_review' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'docs:docs-pr-ai-menu' - ) - ) - uses: ./.github/workflows/docs-aw-pr-ai-menu-collect.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - - route-pr-ai-menu: - needs: prelude - permissions: - actions: read - checks: read - contents: read - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - ( - ( - inputs.ingress-event-name == 'workflow_run' && - fromJSON(inputs.ingress-event-payload-json).workflow_run.conclusion == 'success' && - fromJSON(inputs.ingress-event-payload-json).workflow_run.event == 'pull_request' - ) || - ( - inputs.ingress-event-name == 'issue_comment' && - inputs.ingress-event-action == 'edited' && - fromJSON(inputs.ingress-event-payload-json).issue.pull_request != null - ) || - inputs.ingress-event-name == 'workflow_dispatch' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'docs:docs-pr-ai-menu' - ) - ) - uses: ./.github/workflows/docs-aw-pr-ai-menu.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - unsupported-trigger: - name: Unsupported trigger - needs: prelude - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - !( - ( - inputs.ingress-event-name == 'issues' && - inputs.ingress-event-action == 'opened' - ) || - ( - inputs.ingress-event-name == 'issue_comment' && - inputs.ingress-event-action == 'edited' - ) || - inputs.ingress-event-name == 'workflow_dispatch' || - ( - inputs.ingress-event-name == 'pull_request' && - ( - inputs.ingress-event-action == 'opened' || - inputs.ingress-event-action == 'reopened' || - inputs.ingress-event-action == 'synchronize' || - inputs.ingress-event-action == 'ready_for_review' - ) - ) || - ( - inputs.ingress-event-name == 'workflow_run' && - fromJSON(inputs.ingress-event-payload-json).workflow_run.conclusion == 'success' && - fromJSON(inputs.ingress-event-payload-json).workflow_run.event == 'pull_request' - ) - ) - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - steps: - - name: Unsupported trigger - env: - EVENT_NAME: ${{ inputs.ingress-event-name }} - EVENT_ACTION: ${{ inputs.ingress-event-action }} - TRIGGER_SOURCE: ${{ inputs.trigger-source }} - run: | - echo "Trigger '${TRIGGER_SOURCE}' relayed unsupported event '${EVENT_NAME}' / '${EVENT_ACTION}'." - exit 1 diff --git a/.github/workflows/docs-aw-pr-ai-menu-collect.yml b/.github/workflows/docs-aw-pr-ai-menu-collect.yml index 7f64b1bb..b9225539 100644 --- a/.github/workflows/docs-aw-pr-ai-menu-collect.yml +++ b/.github/workflows/docs-aw-pr-ai-menu-collect.yml @@ -1,23 +1,29 @@ name: Docs PR AI menu collect -# Fork-safe collector for the split-workflow pattern. Routed from docs-aw-ingress on pull_request. -# The PR number artifact is consumed by docs-aw-pr-ai-menu.yml via workflow_run. +# Fork-safe collector for the split-workflow pattern. Consumer repositories install +# `trigger-docs-aw-pull-request.yml`, which calls the pull_request event orchestrator. +# The PR number artifact is consumed by `docs-aw-pr-ai-menu.yml` via `workflow_run`. on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from docs-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true + type: string + shared-allowed-pr-authors-json: + required: true + type: string + shared-allowed-pr-authors-csv: + required: true + type: string + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from docs-aw-ingress - required: false + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from docs-aw-ingress + shared-token-policy: required: true type: string @@ -27,6 +33,7 @@ permissions: jobs: collect: name: Save PR number artifact + if: inputs.shared-proceed == 'true' runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -35,7 +42,7 @@ jobs: steps: - name: Write pull request number env: - PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: echo "$PR_NUMBER" > pr_number.txt - name: Upload pull request number artifact diff --git a/.github/workflows/docs-aw-pr-ai-menu.yml b/.github/workflows/docs-aw-pr-ai-menu.yml index 157a4228..03d0ceee 100644 --- a/.github/workflows/docs-aw-pr-ai-menu.yml +++ b/.github/workflows/docs-aw-pr-ai-menu.yml @@ -1,23 +1,29 @@ name: Docs PR AI menu -# Reusable implementation for the Elastic Docs PR AI menu. Routed from docs-aw-ingress.yml. +# Reusable implementation for the Elastic Docs PR AI menu. Consumer repositories +# install event-scoped client templates under `.github/remote-workflow-template/docs/.github/workflows/`. # Scripts: `scripts/docs/pr-menu/`. on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from docs-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from docs-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true + type: string + shared-allowed-pr-authors-csv: + required: true + type: string + shared-allowed-issue-authors-json: + required: true + type: string + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from docs-aw-ingress + shared-token-policy: required: true type: string secrets: @@ -31,17 +37,11 @@ jobs: post-menu: name: Post or refresh AI PR menu if: >- + inputs.shared-proceed == 'true' && ( - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'workflow_run' && - fromJSON(inputs.ingress-event-payload-json).workflow_run.conclusion == 'success' - ) || - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'workflow_dispatch' + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || + github.event_name == 'workflow_dispatch' + ) runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -51,28 +51,23 @@ jobs: issues: write pull-requests: write concurrency: - group: docs-pr-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).workflow_run.id || fromJSON(inputs.ingress-event-payload-json).pull_request.number || fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.pull_request_number }} + group: docs-pr-ai-menu-${{ github.event.workflow_run.id || github.event.pull_request.number || github.event.issue.number || github.event.inputs.pull_request_number }} cancel-in-progress: false steps: - name: Download pull request number artifact - if: >- - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'workflow_run' + if: github.event_name == 'workflow_run' uses: actions/download-artifact@v8 with: name: pr-number - run-id: ${{ fromJSON(inputs.ingress-event-payload-json).workflow_run.id }} + run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Resolve pull request number id: resolve-pr env: - EVENT_NAME: ${{ inputs.ingress-event-name != '' && inputs.ingress-event-name || github.event_name }} - WORKFLOW_DISPATCH_PR: ${{ fromJSON(inputs.ingress-event-payload-json).inputs.pull_request_number }} + WORKFLOW_DISPATCH_PR: ${{ github.event.inputs.pull_request_number }} run: | - if [ "${EVENT_NAME}" = "workflow_run" ]; then + if [ "${{ github.event_name }}" = "workflow_run" ]; then echo "number=$(cat pr_number.txt)" >> "$GITHUB_OUTPUT" else echo "number=${WORKFLOW_DISPATCH_PR}" >> "$GITHUB_OUTPUT" @@ -86,14 +81,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/pr-menu sparse-checkout-cone-mode: true - name: Create or update AI PR menu comment uses: actions/github-script@v9 env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} PULL_REQUEST_NUMBER: ${{ steps.resolve-pr.outputs.number }} with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -104,15 +97,13 @@ jobs: evaluate-trigger: name: Evaluate AI PR menu trigger if: >- - ( - inputs.ingress-event-name != '' && inputs.ingress-event-name || - github.event_name - ) == 'issue_comment' && - fromJSON(inputs.ingress-event-payload-json).issue.pull_request != null && + inputs.shared-proceed == 'true' && + github.event_name == 'issue_comment' && + github.event.issue.pull_request != null && github.actor != 'github-actions[bot]' && - fromJSON(inputs.ingress-event-payload-json).comment.user.login == 'github-actions[bot]' && - contains(fromJSON(inputs.ingress-event-payload-json).comment.body, '') && - contains(fromJSON(inputs.ingress-event-payload-json).comment.body, '') + github.event.comment.user.login == 'github-actions[bot]' && + contains(github.event.comment.body, '') && + contains(github.event.comment.body, '') runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -129,15 +120,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/pr-menu sparse-checkout-cone-mode: true - name: Parse AI PR menu transition id: evaluate uses: actions/github-script@v9 - env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -156,7 +144,7 @@ jobs: issues: write pull-requests: write concurrency: - group: docs-pr-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number || fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.pull_request_number }} + group: docs-pr-ai-menu-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pull_request_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs PR AI menu scripts @@ -167,14 +155,11 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/pr-menu sparse-checkout-cone-mode: true - name: Refresh AI PR menu status uses: actions/github-script@v9 - env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -183,14 +168,12 @@ jobs: resolve-apm-assets: needs: [evaluate-trigger] - permissions: - contents: read + if: >- + inputs.shared-proceed == 'true' && + needs.evaluate-trigger.outputs.docs_review_triggered == 'true' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: docs-aw-pr-ai-menu.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | This repository stores documentation as markdown across the repository. Prefer concise, high-signal review comments with exact replacement text when possible. @@ -199,6 +182,7 @@ jobs: name: Docs AI / docs review needs: [evaluate-trigger, resolve-apm-assets] if: >- + inputs.shared-proceed == 'true' && needs.evaluate-trigger.outputs.docs_review_triggered == 'true' permissions: actions: read @@ -209,7 +193,6 @@ jobs: with: review-scope: repo-wide-markdown additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -225,7 +208,7 @@ jobs: issues: write pull-requests: write concurrency: - group: docs-pr-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number || fromJSON(inputs.ingress-event-payload-json).issue.number || fromJSON(inputs.ingress-event-payload-json).inputs.pull_request_number }} + group: docs-pr-ai-menu-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pull_request_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs PR AI menu scripts @@ -236,14 +219,12 @@ jobs: path: oblt-aw-scripts fetch-depth: 1 sparse-checkout: | - scripts/docs/lib scripts/docs/pr-menu sparse-checkout-cone-mode: true - name: Refresh AI PR menu status uses: actions/github-script@v9 env: - INGRESS_EVENT_JSON: ${{ inputs.ingress-event-payload-json }} DOCS_REVIEW_RESULT: ${{ needs.run-docs-review.result }} with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/oblt-aw-agent-suggestions.yml b/.github/workflows/oblt-aw-agent-suggestions.yml index ac5653cb..162566e6 100644 --- a/.github/workflows/oblt-aw-agent-suggestions.yml +++ b/.github/workflows/oblt-aw-agent-suggestions.yml @@ -2,30 +2,25 @@ name: Agent Suggestions on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -34,15 +29,11 @@ permissions: contents: read jobs: + resolve-apm-assets: - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-agent-suggestions.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Additional requirements for this repository: @@ -69,5 +60,4 @@ jobs: with: title-prefix: "[oblt-aw][agent-suggestions]" additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: inherit diff --git a/.github/workflows/oblt-aw-autodoc.yml b/.github/workflows/oblt-aw-autodoc.yml index 911bdc4e..13738b9e 100644 --- a/.github/workflows/oblt-aw-autodoc.yml +++ b/.github/workflows/oblt-aw-autodoc.yml @@ -2,30 +2,25 @@ name: Automated Documentation Analysis and Improvement on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true + type: string + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -37,14 +32,9 @@ jobs: # Step 1: Detect docs drift from recent code changes and create an issue with findings resolve-apm-assets-audit: - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-autodoc.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to analyze ALL documentation in this repository, identify gaps and areas for improvement, and file an issue with concrete findings. @@ -84,20 +74,14 @@ jobs: lookback-window: 1 day ago title-prefix: "[oblt-aw][autodoc]" additional-instructions: ${{ needs.resolve-apm-assets-audit.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets-audit.outputs.resolved-setup-commands-json), ' && ') }} secrets: inherit resolve-apm-assets-fix: needs: audit if: needs.audit.outputs.created_issue_number != '' - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-autodoc.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to implement the documentation improvements described in the issue. @@ -145,7 +129,6 @@ jobs: target-issue-number: ${{ needs.audit.outputs.created_issue_number }} draft-prs: true additional-instructions: ${{ needs.resolve-apm-assets-fix.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets-fix.outputs.resolved-setup-commands-json), ' && ') }} secrets: inherit # Step 3: Finalize autodoc PR — assign reviewer and apply labels diff --git a/.github/workflows/oblt-aw-automerge.yml b/.github/workflows/oblt-aw-automerge.yml index 89f75130..3cff724f 100644 --- a/.github/workflows/oblt-aw-automerge.yml +++ b/.github/workflows/oblt-aw-automerge.yml @@ -2,30 +2,25 @@ name: Automerge on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true + type: string + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-issue-authors-json: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: description: "Token for GH-AW mention-in-pr (Copilot); must be supplied by the ingress caller." @@ -36,7 +31,7 @@ permissions: jobs: - # Single PR from fromJSON(inputs.ingress-event-payload-json).pull_request (relayed consumer event). + # Single PR from github.event.pull_request. PR fields only (pull_request trigger). verify: runs-on: ubuntu-latest permissions: @@ -76,7 +71,7 @@ jobs: id: validate uses: actions/github-script@v9 env: - PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }} + PR_NUMBER: ${{ github.event.pull_request.number }} with: github-token: ${{ github.token }} script: | @@ -135,7 +130,7 @@ jobs: id: check-collection uses: actions/github-script@v9 env: - PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }} + PR_NUMBER: ${{ github.event.pull_request.number }} with: github-token: ${{ github.token }} script: | @@ -155,16 +150,11 @@ jobs: if: >- needs.verify.outputs.proceed == 'true' && needs.check-dependency-collection.outputs.allowed == 'true' - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-automerge.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | - Target pull request number: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }}. + Target pull request number: ${{ github.event.pull_request.number }}. approve: needs: [verify, check-dependency-collection, resolve-apm-assets] @@ -179,11 +169,10 @@ jobs: pull-requests: write uses: elastic/ai-github-actions/.github/workflows/gh-aw-mention-in-pr.lock.yml@main with: - allowed-bot-users: ${{ inputs.ingress-allowed-pr-authors-csv }} + allowed-bot-users: ${{ inputs.shared-allowed-pr-authors-csv }} additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} prompt: | - For pull request #${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }}: evaluate automerge eligibility. + For pull request #${{ github.event.pull_request.number }}: evaluate automerge eligibility. This PR already passed automerge validation in the workflow (GITHUB_TOKEN) for author, label, draft/fork/ref, and dependency collection `${{ needs.check-dependency-collection.outputs.collection-id }}` (file-path classification; no extra repo labels). Do not call check-run or commit status APIs. Required checks are enforced by GitHub when auto-merge is enabled. @@ -223,7 +212,7 @@ jobs: uses: pascalgn/automerge-action@7961b8b5eec56cc088c140b56d864285eabd3f67 # v0.16.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PULL_REQUEST: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }} + PULL_REQUEST: ${{ github.event.pull_request.number }} MERGE_LABELS: "oblt-aw/ai/merge-ready" UPDATE_LABELS: "oblt-aw/ai/merge-ready" MERGE_METHOD: squash @@ -244,14 +233,22 @@ jobs: permissions: id-token: write steps: - - name: Create ephemeral GitHub token - id: create-token + - name: Create ephemeral GitHub token (configured policy) + id: create-token-explicit + if: ${{ inputs.shared-token-policy != '' }} + uses: elastic/oblt-actions/github/create-token@v1 + with: + token-policy: ${{ inputs.shared-token-policy }} + + - name: Create ephemeral GitHub token (Vault auto policy) + id: create-token-auto + if: ${{ inputs.shared-token-policy == '' }} uses: elastic/oblt-actions/github/create-token@v1 - name: Enable merge when ready (merge queue fallback) env: - GH_TOKEN: ${{ steps.create-token.outputs.token }} - PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }} + GH_TOKEN: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} run: | set -euo pipefail diff --git a/.github/workflows/oblt-aw-dependency-review.yml b/.github/workflows/oblt-aw-dependency-review.yml index 6b577447..ca0026af 100644 --- a/.github/workflows/oblt-aw-dependency-review.yml +++ b/.github/workflows/oblt-aw-dependency-review.yml @@ -2,30 +2,25 @@ name: Dependency Review on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -36,14 +31,9 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-dependency-review.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Noop when not applicable (mandatory): - If the PR has NO dependency updates to review (e.g. no version bumps in manifest files, no changes to lockfiles that indicate dependency updates, or changes that do not match any supported ecosystem), you MUST call `noop` — do NOT create any comment. @@ -82,10 +72,9 @@ jobs: pull-requests: write uses: elastic/ai-github-actions/.github/workflows/gh-aw-dependency-review.lock.yml@main with: - allowed-bot-users: ${{ inputs.ingress-allowed-pr-authors-csv }} + allowed-bot-users: ${{ inputs.shared-allowed-pr-authors-csv }} classification-labels: "oblt-aw/ai/merge-ready" additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -102,24 +91,26 @@ jobs: id-token: write pull-requests: write steps: - - name: Create ephemeral GitHub token - id: create-token + - name: Create ephemeral GitHub token (configured policy) + id: create-token-explicit + if: ${{ inputs.shared-token-policy != '' }} + uses: elastic/oblt-actions/github/create-token@v1 + with: + token-policy: ${{ inputs.shared-token-policy }} + + - name: Create ephemeral GitHub token (Vault auto policy) + id: create-token-auto + if: ${{ inputs.shared-token-policy == '' }} uses: elastic/oblt-actions/github/create-token@v1 - name: Re-apply merge-ready label to emit installation-token labeled event uses: actions/github-script@v9 - env: - PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).pull_request.number }} with: - github-token: ${{ steps.create-token.outputs.token }} + github-token: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; - const prNumber = Number(process.env.PR_NUMBER); - if (!Number.isFinite(prNumber) || prNumber <= 0) { - core.info('No pull_request.number in relayed event; skipping re-apply.'); - return; - } + const prNumber = context.payload.pull_request.number; const { data: issue } = await github.rest.issues.get({ owner, repo, diff --git a/.github/workflows/oblt-aw-duplicate-issue-detector.yml b/.github/workflows/oblt-aw-duplicate-issue-detector.yml index 47c7797e..0d4d2bcf 100644 --- a/.github/workflows/oblt-aw-duplicate-issue-detector.yml +++ b/.github/workflows/oblt-aw-duplicate-issue-detector.yml @@ -3,30 +3,25 @@ name: Duplicate Issue Detector on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -35,15 +30,16 @@ permissions: contents: read jobs: + resolve-apm-assets: - permissions: - contents: read + if: >- + ( + (github.event_name == 'issues' && github.event.action == 'opened') || + github.event_name == 'workflow_dispatch' + ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-duplicate-issue-detector.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} duplicate-issue-detector: needs: [resolve-apm-assets] diff --git a/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml b/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml index 41ba67dc..3589d873 100644 --- a/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml +++ b/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml @@ -3,30 +3,25 @@ name: PR Buildkite Detective on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true + type: string + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-issue-authors-json: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -39,14 +34,13 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'status' && + github.event.state == 'failure' && + contains(github.event.context, 'buildkite') uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-estc-pr-buildkite-detective.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | If a step fails, check if the failure is reported as a GitHub issue labeled `flaky-test`. Reference the GitHub issue if so. @@ -57,14 +51,9 @@ jobs: contents: read issues: read pull-requests: write - if: >- - github.event_name == 'status' && - github.event.state == 'failure' && - contains(github.event.context, 'buildkite') uses: elastic/ai-github-actions/.github/workflows/gh-aw-estc-pr-buildkite-detective.lock.yml@copilot/reduce-comment-spamming with: additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }} diff --git a/.github/workflows/oblt-aw-event-issue-comment.yml b/.github/workflows/oblt-aw-event-issue-comment.yml new file mode 100644 index 00000000..bd04ff75 --- /dev/null +++ b/.github/workflows/oblt-aw-event-issue-comment.yml @@ -0,0 +1,76 @@ +name: Observability Agentic Workflow — Issue Comment Event + +# Event-scoped orchestrator: one shared prelude chain for all issue_comment routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: true + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: >- + ["oblt-aw-issue-fixer.yml","oblt-aw-mention-in-issue.yml"] + load-allowed-authors: false + + issue-fixer: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-issue-fixer.yml'] == 'true' && + github.event_name == 'issue_comment' && + github.event.action == 'created' && + github.event.issue.pull_request == null && + startsWith(github.event.comment.body, '/ai implement') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) && + !contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/security-') && + !contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/res-not-accessible-by-integration') + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-issue-fixer.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-issue-fixer.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: inherit + + mention-in-issue: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-mention-in-issue.yml'] == 'true' && + github.event_name == 'issue_comment' && + github.event.action == 'created' && + github.event.issue.pull_request == null && + startsWith(github.event.comment.body, '/ai') && + !startsWith(github.event.comment.body, '/ai implement') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-mention-in-issue.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-mention-in-issue.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/oblt-aw-event-issues.yml b/.github/workflows/oblt-aw-event-issues.yml new file mode 100644 index 00000000..e785ce95 --- /dev/null +++ b/.github/workflows/oblt-aw-event-issues.yml @@ -0,0 +1,173 @@ +name: Observability Agentic Workflow — Issues Event + +# Event-scoped orchestrator: one shared prelude chain for all issues/workflow_dispatch routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: true + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: >- + ["oblt-aw-issue-triage.yml","oblt-aw-duplicate-issue-detector.yml","oblt-aw-security-triage.yml","oblt-aw-security-fixer.yml","oblt-aw-resource-not-accessible-by-integration-triage.yml","oblt-aw-resource-not-accessible-by-integration-fixer.yml"] + load-allowed-authors: true + + issue-triage: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-issue-triage.yml'] == 'true' && + github.event_name == 'issues' && + github.event.action == 'opened' + permissions: + actions: read + contents: read + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-issue-triage.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-issue-triage.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + duplicate-issue-detector: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-duplicate-issue-detector.yml'] == 'true' && + ( + (github.event_name == 'issues' && github.event.action == 'opened') || + github.event_name == 'workflow_dispatch' + ) + permissions: + contents: read + issues: write + pull-requests: read + uses: ./.github/workflows/oblt-aw-duplicate-issue-detector.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-duplicate-issue-detector.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + security-triage: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-security-triage.yml'] == 'true' && + github.event_name == 'issues' && + ( + (github.event.action == 'opened' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/detector/security')) || + (github.event.action == 'labeled' && github.event.label.name == 'oblt-aw/detector/security') + ) + permissions: + actions: read + contents: read + discussions: write + id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-security-triage.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-security-triage.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + security-fixer: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-security-fixer.yml'] == 'true' && + github.event_name == 'issues' && + github.event.action == 'labeled' && + ( + (github.event.label.name == 'oblt-aw/ai/fix-ready' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/security-')) || + (startsWith(github.event.label.name, 'oblt-aw/triage/security-') && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/ai/fix-ready')) + ) + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-security-fixer.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-security-fixer.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + resource-not-accessible-triage: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-resource-not-accessible-by-integration-triage.yml'] == 'true' && + github.event_name == 'issues' && + ( + (github.event.action == 'opened' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/detector/res-not-accessible-by-integration')) || + (github.event.action == 'labeled' && github.event.label.name == 'oblt-aw/detector/res-not-accessible-by-integration') + ) + permissions: + actions: read + contents: read + discussions: write + id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-resource-not-accessible-by-integration-triage.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + resource-not-accessible-fixer: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-resource-not-accessible-by-integration-fixer.yml'] == 'true' && + github.event_name == 'issues' && + github.event.action == 'labeled' && + github.event.label.name == 'oblt-aw/ai/fix-ready' && + contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/res-not-accessible-by-integration') + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-resource-not-accessible-by-integration-fixer.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: inherit diff --git a/.github/workflows/oblt-aw-event-pull-request.yml b/.github/workflows/oblt-aw-event-pull-request.yml new file mode 100644 index 00000000..47a7689b --- /dev/null +++ b/.github/workflows/oblt-aw-event-pull-request.yml @@ -0,0 +1,72 @@ +name: Observability Agentic Workflow — Pull Request Event + +# Event-scoped orchestrator: one shared prelude chain for all pull_request routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: true + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: >- + ["oblt-aw-automerge.yml","oblt-aw-dependency-review.yml"] + load-allowed-authors: true + + automerge: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-automerge.yml'] == 'true' && + github.event_name == 'pull_request' && + contains(fromJSON('["opened","synchronize","reopened","labeled"]'), github.event.action) && + contains(fromJSON(needs.prelude.outputs.allowed-pr-authors-json), github.event.pull_request.user.login) && + contains(join(github.event.pull_request.labels.*.name, ','), 'oblt-aw/ai/merge-ready') + permissions: + actions: read + contents: write + discussions: write + id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-automerge.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-automerge.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + dependency-review: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-dependency-review.yml'] == 'true' && + github.event_name == 'pull_request' && + contains(fromJSON('["opened","synchronize","reopened"]'), github.event.action) && + contains(fromJSON(needs.prelude.outputs.allowed-pr-authors-json), github.event.pull_request.user.login) + permissions: + actions: read + contents: read + id-token: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-dependency-review.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-dependency-review.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/oblt-aw-event-schedule.yml b/.github/workflows/oblt-aw-event-schedule.yml new file mode 100644 index 00000000..64d36cb8 --- /dev/null +++ b/.github/workflows/oblt-aw-event-schedule.yml @@ -0,0 +1,101 @@ +name: Observability Agentic Workflow — Schedule Event + +# Event-scoped orchestrator: one shared prelude chain for all schedule/workflow_dispatch routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: true + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: >- + ["oblt-aw-agent-suggestions.yml","oblt-aw-autodoc.yml","oblt-aw-security-detector.yml","oblt-aw-resource-not-accessible-by-integration-detector.yml"] + load-allowed-authors: false + + agent-suggestions: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-agent-suggestions.yml'] == 'true' + permissions: + contents: read + issues: write + pull-requests: read + uses: ./.github/workflows/oblt-aw-agent-suggestions.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-agent-suggestions.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + autodoc: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-autodoc.yml'] == 'true' + permissions: + actions: read + contents: write + issues: write + pull-requests: write + uses: ./.github/workflows/oblt-aw-autodoc.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-autodoc.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + + security-detector: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-security-detector.yml'] == 'true' && + contains(fromJSON('["schedule","workflow_dispatch"]'), github.event_name) + permissions: + actions: read + contents: read + id-token: write + issues: read + pull-requests: read + uses: ./.github/workflows/oblt-aw-security-detector.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-security-detector.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + + resource-not-accessible-detector: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-resource-not-accessible-by-integration-detector.yml'] == 'true' && + github.event_name == 'schedule' + permissions: + actions: read + contents: read + issues: write + uses: ./.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-resource-not-accessible-by-integration-detector.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/oblt-aw-event-status.yml b/.github/workflows/oblt-aw-event-status.yml new file mode 100644 index 00000000..7dc07da7 --- /dev/null +++ b/.github/workflows/oblt-aw-event-status.yml @@ -0,0 +1,47 @@ +name: Observability Agentic Workflow — Status Event + +# Event-scoped orchestrator for status-triggered routes. +on: + workflow_call: + secrets: + COPILOT_GITHUB_TOKEN: + required: true + BUILDKITE_API_TOKEN: + required: true + +permissions: + contents: read + +jobs: + prelude: + permissions: + contents: read + issues: read + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflows: '["oblt-aw-estc-pr-buildkite-detective.yml"]' + load-allowed-authors: false + + estc-pr-buildkite-detective: + needs: prelude + if: >- + fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-estc-pr-buildkite-detective.yml'] == 'true' && + github.event_name == 'status' && + github.event.state == 'failure' && + contains(github.event.context, 'buildkite') + permissions: + actions: read + contents: read + issues: read + pull-requests: write + uses: ./.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml + with: + shared-proceed: ${{ fromJSON(needs.prelude.outputs.proceed-by-workflow)['oblt-aw-estc-pr-buildkite-detective.yml'] }} + shared-allowed-pr-authors-json: ${{ needs.prelude.outputs.allowed-pr-authors-json }} + shared-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + shared-allowed-issue-authors-json: ${{ needs.prelude.outputs.allowed-issue-authors-json }} + shared-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + shared-token-policy: ${{ needs.prelude.outputs.token-policy }} + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }} diff --git a/.github/workflows/oblt-aw-ingress.yml b/.github/workflows/oblt-aw-ingress.yml deleted file mode 100644 index 7f6df8c2..00000000 --- a/.github/workflows/oblt-aw-ingress.yml +++ /dev/null @@ -1,611 +0,0 @@ -name: Observability Agentic Workflow Ingress - -# Dynamic routing: each route-* job carries its own if: gate; only eligible workflows run. -# Event context is relayed from trigger-oblt-aw.yml via oblt-aw.yml workflow_dispatch inputs. -on: - workflow_call: - inputs: - trigger-source: - description: Client trigger workflow that dispatched the entrypoint - required: true - type: string - ingress-event-name: - description: Original github.event_name from the client trigger - required: true - type: string - ingress-event-action: - description: Original github.event.action from the client trigger - required: false - type: string - default: '' - ingress-event-payload-json: - description: JSON-encoded original github.event from the client trigger - required: true - type: string - caller-ref: - description: Original github.ref from the client trigger - required: true - type: string - caller-sha: - description: Original github.sha from the client trigger - required: true - type: string - caller-run-id: - description: Original github.run_id from the client trigger - required: true - type: string - secrets: - COPILOT_GITHUB_TOKEN: - required: false - BUILDKITE_API_TOKEN: - required: false - -permissions: - contents: read - -jobs: - prelude: - permissions: - contents: read - issues: read - if: >- - inputs.ingress-event-name != 'status' || - ( - inputs.ingress-event-name == 'status' && - fromJSON(inputs.ingress-event-payload-json).state == 'failure' && - contains(fromJSON(inputs.ingress-event-payload-json).context, 'buildkite') - ) - uses: ./.github/workflows/aw-prelude.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - load-allowed-authors: ${{ inputs.ingress-event-name == 'pull_request' || inputs.ingress-event-name == 'issues' }} - - route-agent-suggestions: - needs: prelude - permissions: - contents: read - issues: write - pull-requests: read - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'schedule' && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:agent-suggestions') - ) - uses: ./.github/workflows/oblt-aw-agent-suggestions.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-autodoc: - needs: prelude - permissions: - actions: read - contents: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'schedule' && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:autodoc') - ) - uses: ./.github/workflows/oblt-aw-autodoc.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: inherit - - route-automerge: - needs: prelude - permissions: - actions: read - contents: write - discussions: write - id-token: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'pull_request' && - ( - inputs.ingress-event-action == 'opened' || - inputs.ingress-event-action == 'synchronize' || - inputs.ingress-event-action == 'reopened' || - inputs.ingress-event-action == 'labeled' - ) && - contains( - fromJSON(needs.prelude.outputs.allowed-pr-authors-json || '[]'), - fromJSON(inputs.ingress-event-payload-json).pull_request.user.login - ) && - contains( - fromJSON(inputs.ingress-event-payload-json).pull_request.labels.*.name, - 'oblt-aw/ai/merge-ready' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:automerge') - ) - uses: ./.github/workflows/oblt-aw-automerge.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-dependency-review: - needs: prelude - permissions: - actions: read - contents: read - id-token: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'pull_request' && - ( - inputs.ingress-event-action == 'opened' || - inputs.ingress-event-action == 'synchronize' || - inputs.ingress-event-action == 'reopened' - ) && - contains( - fromJSON(needs.prelude.outputs.allowed-pr-authors-json || '[]'), - fromJSON(inputs.ingress-event-payload-json).pull_request.user.login - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:dependency-review') - ) - uses: ./.github/workflows/oblt-aw-dependency-review.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-duplicate-issue-detector: - needs: prelude - permissions: - contents: read - issues: write - pull-requests: read - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - ( - ( - inputs.ingress-event-name == 'issues' && - inputs.ingress-event-action == 'opened' - ) || - inputs.ingress-event-name == 'workflow_dispatch' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'obs:duplicate-issue-detector' - ) - ) - uses: ./.github/workflows/oblt-aw-duplicate-issue-detector.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-issue-triage: - needs: prelude - permissions: - actions: read - contents: read - discussions: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issues' && - inputs.ingress-event-action == 'opened' && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:issue-triage') - ) - uses: ./.github/workflows/oblt-aw-issue-triage.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-issue-fixer: - needs: prelude - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issue_comment' && - inputs.ingress-event-action == 'created' && - fromJSON(inputs.ingress-event-payload-json).issue.pull_request == null && - startsWith(fromJSON(inputs.ingress-event-payload-json).comment.body, '/ai implement') && - ( - fromJSON(inputs.ingress-event-payload-json).comment.author_association == 'OWNER' || - fromJSON(inputs.ingress-event-payload-json).comment.author_association == 'MEMBER' || - fromJSON(inputs.ingress-event-payload-json).comment.author_association == 'COLLABORATOR' - ) && - !contains( - join(fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, ','), - 'oblt-aw/triage/security-' - ) && - !contains( - join(fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, ','), - 'oblt-aw/triage/res-not-accessible-by-integration' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:issue-fixer') - ) - uses: ./.github/workflows/oblt-aw-issue-fixer.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: inherit - - route-mention-in-issue: - needs: prelude - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issue_comment' && - inputs.ingress-event-action == 'created' && - fromJSON(inputs.ingress-event-payload-json).issue.pull_request == null && - startsWith(fromJSON(inputs.ingress-event-payload-json).comment.body, '/ai') && - !startsWith(fromJSON(inputs.ingress-event-payload-json).comment.body, '/ai implement') && - ( - fromJSON(inputs.ingress-event-payload-json).comment.author_association == 'OWNER' || - fromJSON(inputs.ingress-event-payload-json).comment.author_association == 'MEMBER' || - fromJSON(inputs.ingress-event-payload-json).comment.author_association == 'COLLABORATOR' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:mention-in-issue') - ) - uses: ./.github/workflows/oblt-aw-mention-in-issue.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-security-detector: - needs: prelude - permissions: - actions: read - contents: read - id-token: write - pull-requests: read - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - ( - inputs.ingress-event-name == 'schedule' || - inputs.ingress-event-name == 'workflow_dispatch' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:security') - ) - uses: ./.github/workflows/oblt-aw-security-detector.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - - route-security-fixer: - needs: prelude - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issues' && - inputs.ingress-event-action == 'labeled' && - ( - ( - fromJSON(inputs.ingress-event-payload-json).label.name == 'oblt-aw/ai/fix-ready' && - contains( - join(fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, ','), - 'oblt-aw/triage/security-' - ) - ) || - ( - startsWith(fromJSON(inputs.ingress-event-payload-json).label.name, 'oblt-aw/triage/security-') && - contains( - fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, - 'oblt-aw/ai/fix-ready' - ) - ) - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:security') - ) - uses: ./.github/workflows/oblt-aw-security-fixer.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: inherit - - route-security-triage: - needs: prelude - permissions: - actions: read - contents: read - discussions: write - id-token: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issues' && - ( - ( - inputs.ingress-event-action == 'opened' && - contains( - fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, - 'oblt-aw/detector/security' - ) - ) || - ( - inputs.ingress-event-action == 'labeled' && - fromJSON(inputs.ingress-event-payload-json).label.name == 'oblt-aw/detector/security' - ) - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains(fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), 'obs:security') - ) - uses: ./.github/workflows/oblt-aw-security-triage.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-resource-not-accessible-by-integration-detector: - needs: prelude - permissions: - actions: read - contents: read - issues: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'schedule' && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'obs:resource-not-accessible-by-integration' - ) - ) - uses: ./.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-resource-not-accessible-by-integration-fixer: - needs: prelude - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issues' && - inputs.ingress-event-action == 'labeled' && - fromJSON(inputs.ingress-event-payload-json).label.name == 'oblt-aw/ai/fix-ready' && - contains( - join(fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, ','), - 'oblt-aw/triage/res-not-accessible-by-integration' - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'obs:resource-not-accessible-by-integration' - ) - ) - uses: ./.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: inherit - - route-resource-not-accessible-by-integration-triage: - needs: prelude - permissions: - actions: read - contents: read - discussions: write - id-token: write - issues: write - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'issues' && - ( - ( - inputs.ingress-event-action == 'opened' && - contains( - fromJSON(inputs.ingress-event-payload-json).issue.labels.*.name, - 'oblt-aw/detector/res-not-accessible-by-integration' - ) - ) || - ( - inputs.ingress-event-action == 'labeled' && - fromJSON(inputs.ingress-event-payload-json).label.name == - 'oblt-aw/detector/res-not-accessible-by-integration' - ) - ) && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'obs:resource-not-accessible-by-integration' - ) - ) - uses: ./.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - - route-estc-pr-buildkite-detective: - needs: prelude - permissions: - actions: read - contents: read - issues: read - pull-requests: write - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - inputs.ingress-event-name == 'status' && - fromJSON(inputs.ingress-event-payload-json).state == 'failure' && - contains(fromJSON(inputs.ingress-event-payload-json).context, 'buildkite') && - ( - needs.prelude.outputs.effective-raw == '' || - contains( - fromJSON(needs.prelude.outputs.enabled-workflows || '[]'), - 'obs:estc-pr-buildkite-detective' - ) - ) - uses: ./.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml - with: - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} - ingress-allowed-pr-authors-csv: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} - ingress-allowed-issue-authors-csv: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }} - - unsupported-trigger: - name: Unsupported trigger - needs: prelude - if: >- - always() && - (needs.prelude.result == 'success' || needs.prelude.result == 'skipped') && - !( - inputs.ingress-event-name == 'schedule' || - inputs.ingress-event-name == 'workflow_call' || - inputs.ingress-event-name == 'workflow_dispatch' || - inputs.ingress-event-name == 'status' || - ( - inputs.ingress-event-name == 'issues' && - ( - inputs.ingress-event-action == 'opened' || - inputs.ingress-event-action == 'labeled' - ) - ) || - ( - inputs.ingress-event-name == 'issue_comment' && - inputs.ingress-event-action == 'created' - ) || - ( - inputs.ingress-event-name == 'pull_request' && - ( - inputs.ingress-event-action == 'opened' || - inputs.ingress-event-action == 'synchronize' || - inputs.ingress-event-action == 'reopened' || - inputs.ingress-event-action == 'labeled' - ) - ) - ) - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - steps: - - name: Unsupported trigger - env: - EVENT_NAME: ${{ inputs.ingress-event-name }} - EVENT_ACTION: ${{ inputs.ingress-event-action }} - TRIGGER_SOURCE: ${{ inputs.trigger-source }} - run: | - echo "Trigger '${TRIGGER_SOURCE}' relayed unsupported event '${EVENT_NAME}' / '${EVENT_ACTION}'." - exit 1 diff --git a/.github/workflows/oblt-aw-issue-fixer.yml b/.github/workflows/oblt-aw-issue-fixer.yml index af57ef04..55a5da89 100644 --- a/.github/workflows/oblt-aw-issue-fixer.yml +++ b/.github/workflows/oblt-aw-issue-fixer.yml @@ -2,30 +2,25 @@ name: Issue Fixer on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' permissions: contents: read @@ -33,14 +28,15 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event.issue.pull_request == null && + startsWith(github.event.comment.body, '/ai implement') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) && + !contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/security-') && + !contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/res-not-accessible-by-integration') uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-issue-fixer.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to fix issues requested through `/ai implement` comments. @@ -94,7 +90,6 @@ jobs: uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-fixer.lock.yml@main with: additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: inherit request-reviewers: diff --git a/.github/workflows/oblt-aw-issue-triage.yml b/.github/workflows/oblt-aw-issue-triage.yml index d12efadc..5ff79e24 100644 --- a/.github/workflows/oblt-aw-issue-triage.yml +++ b/.github/workflows/oblt-aw-issue-triage.yml @@ -3,30 +3,25 @@ name: Issue Triage on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true + type: string + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-issue-authors-json: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -37,14 +32,12 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'issues' && + github.event.action == 'opened' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-issue-triage.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} issue-triage: needs: [resolve-apm-assets] @@ -57,6 +50,5 @@ jobs: uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-triage.lock.yml@main with: additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/oblt-aw-mention-in-issue.yml b/.github/workflows/oblt-aw-mention-in-issue.yml index fefcb1b7..b5f19f18 100644 --- a/.github/workflows/oblt-aw-mention-in-issue.yml +++ b/.github/workflows/oblt-aw-mention-in-issue.yml @@ -3,30 +3,25 @@ name: Mention in Issue on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -37,23 +32,19 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'issue_comment' && + github.event.action == 'created' && + github.event.issue.pull_request == null && + startsWith(github.event.comment.body, '/ai') && + !startsWith(github.event.comment.body, '/ai implement') && + contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-mention-in-issue.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} mention-in-issue: needs: [resolve-apm-assets] - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write if: >- github.event_name == 'issue_comment' && github.event.action == 'created' && @@ -61,9 +52,14 @@ jobs: startsWith(github.event.comment.body, '/ai') && !startsWith(github.event.comment.body, '/ai implement') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write uses: elastic/ai-github-actions/.github/workflows/gh-aw-mention-in-issue.lock.yml@main with: additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml b/.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml index 0f150e92..8d018b39 100644 --- a/.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml +++ b/.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml @@ -2,30 +2,25 @@ name: Resource Not Accessible by Integration Detector on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true + type: string + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-issue-authors-json: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -56,14 +51,9 @@ jobs: resolve-apm-assets: needs: [discover] if: needs.discover.result == 'success' && needs.discover.outputs.workflows != '[]' - permissions: - contents: read uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-detector.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | **Error Context:** You are analyzing preflight search results for the exact phrase "Resource not accessible by integration" in GitHub Actions workflow logs. This error typically indicates permission or token scope issues when workflows access protected resources (e.g., secrets, environments, or repository settings). diff --git a/.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml b/.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml index 64f667f8..eb3a631a 100644 --- a/.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml +++ b/.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml @@ -2,30 +2,25 @@ name: Resource not accessible by Integration Issue Fixer on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true + type: string + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-csv: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-issue-authors-json: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' permissions: contents: read @@ -33,14 +28,14 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'issues' && + github.event.action == 'labeled' && + github.event.label.name == 'oblt-aw/ai/fix-ready' && + contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/res-not-accessible-by-integration') uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-fixer.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to fix issues labeled for the "Resource not accessible by integration" problem. @@ -93,9 +88,8 @@ jobs: contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/res-not-accessible-by-integration') uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-fixer.lock.yml@main with: - allowed-bot-users: ${{ inputs.ingress-allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.shared-allowed-issue-authors-csv }} additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: inherit request-reviewers: diff --git a/.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml b/.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml index 7770cd03..f73e6aa6 100644 --- a/.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml +++ b/.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml @@ -2,49 +2,46 @@ name: Resource not accessible by Integration Issue Triage on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true # Ephemeral token minting for label API runs only in signal-res-not-accessible-triage-followups (see #4374 workaround). +# Omit token-policy on create-token so Vault uses the auto role token-policy-<12-char sha256(workflow ref base)>; register in catalog-info. permissions: contents: read jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'issues' && + ( + (github.event.action == 'opened' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/detector/res-not-accessible-by-integration')) || + (github.event.action == 'labeled' && github.event.label.name == 'oblt-aw/detector/res-not-accessible-by-integration') + ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-triage.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to triage issues that carry the detector label `oblt-aw/detector/res-not-accessible-by-integration` and determine if they are related to "Resource not accessible by integration" errors in GitHub Actions workflows. @@ -238,12 +235,17 @@ jobs: discussions: write issues: write pull-requests: write + if: >- + github.event_name == 'issues' && + ( + (github.event.action == 'opened' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/detector/res-not-accessible-by-integration')) || + (github.event.action == 'labeled' && github.event.label.name == 'oblt-aw/detector/res-not-accessible-by-integration') + ) uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-triage.lock.yml@main with: - allowed-bot-users: ${{ inputs.ingress-allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.shared-allowed-issue-authors-csv }} classification-labels: "oblt-aw/triage/res-not-accessible-by-integration,oblt-aw/triage/other,oblt-aw/triage/needs-info,oblt-aw/ai/fix-ready" additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -259,24 +261,26 @@ jobs: id-token: write issues: write steps: - - name: Create ephemeral GitHub token - id: create-token + - name: Create ephemeral GitHub token (configured policy) + id: create-token-explicit + if: ${{ inputs.shared-token-policy != '' }} + uses: elastic/oblt-actions/github/create-token@v1 + with: + token-policy: ${{ inputs.shared-token-policy }} + + - name: Create ephemeral GitHub token (Vault auto policy) + id: create-token-auto + if: ${{ inputs.shared-token-policy == '' }} uses: elastic/oblt-actions/github/create-token@v1 - name: Re-apply fix-ready label to emit installation-token labeled event uses: actions/github-script@v9 - env: - ISSUE_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).issue.number }} with: - github-token: ${{ steps.create-token.outputs.token }} + github-token: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; - const issueNumber = Number(process.env.ISSUE_NUMBER); - if (!Number.isFinite(issueNumber) || issueNumber <= 0) { - core.info('No issue.number in relayed event; skipping re-apply.'); - return; - } + const issueNumber = context.issue.number; const { data: issue } = await github.rest.issues.get({ owner, repo, diff --git a/.github/workflows/oblt-aw-security-detector.yml b/.github/workflows/oblt-aw-security-detector.yml index 3d19ab81..40274370 100644 --- a/.github/workflows/oblt-aw-security-detector.yml +++ b/.github/workflows/oblt-aw-security-detector.yml @@ -3,30 +3,25 @@ name: Security Detector on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true + type: string + shared-allowed-issue-authors-csv: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-token-policy: + required: true type: string - default: '' permissions: contents: read @@ -71,15 +66,26 @@ jobs: count=$(grep -cve '^[[:space:]]*$' findings.txt 2>/dev/null || echo 0) echo "count=$count" >> "$GITHUB_OUTPUT" - - name: Create ephemeral GitHub token - id: create-token - if: ${{ steps.scan.outputs.count != '0' }} + - name: Create ephemeral GitHub token (configured policy) + id: create-token-explicit + if: >- + steps.scan.outputs.count != '0' && + inputs.shared-token-policy != '' + uses: elastic/oblt-actions/github/create-token@v1 + with: + token-policy: ${{ inputs.shared-token-policy }} + + - name: Create ephemeral GitHub token (Vault auto policy) + id: create-token-auto + if: >- + steps.scan.outputs.count != '0' && + inputs.shared-token-policy == '' uses: elastic/oblt-actions/github/create-token@v1 - name: Create issues from findings if: ${{ steps.scan.outputs.count != '0' }} env: - GH_TOKEN: ${{ steps.create-token.outputs.token }} + GH_TOKEN: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} GITHUB_REPOSITORY: ${{ github.repository }} run: | SECURITY_SCAN_DATE="$(date -u +%Y-%m-%d)" diff --git a/.github/workflows/oblt-aw-security-fixer.yml b/.github/workflows/oblt-aw-security-fixer.yml index da08d707..91b2eed6 100644 --- a/.github/workflows/oblt-aw-security-fixer.yml +++ b/.github/workflows/oblt-aw-security-fixer.yml @@ -2,30 +2,25 @@ name: Security Issue Fixer on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -36,14 +31,16 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'issues' && + github.event.action == 'labeled' && + ( + (github.event.label.name == 'oblt-aw/ai/fix-ready' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/security-')) || + (startsWith(github.event.label.name, 'oblt-aw/triage/security-') && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/ai/fix-ready')) + ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-security-fixer.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to fix security issues labeled for remediation. This workflow uses agentic workflows from elastic/ai-github-actions. @@ -90,11 +87,17 @@ jobs: discussions: write issues: write pull-requests: write + if: >- + github.event_name == 'issues' && + github.event.action == 'labeled' && + ( + (github.event.label.name == 'oblt-aw/ai/fix-ready' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/triage/security-')) || + (startsWith(github.event.label.name, 'oblt-aw/triage/security-') && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/ai/fix-ready')) + ) uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-fixer.lock.yml@main with: - allowed-bot-users: ${{ inputs.ingress-allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.shared-allowed-issue-authors-csv }} additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: inherit request-reviewers: diff --git a/.github/workflows/oblt-aw-security-triage.yml b/.github/workflows/oblt-aw-security-triage.yml index a8a68307..99ee69a3 100644 --- a/.github/workflows/oblt-aw-security-triage.yml +++ b/.github/workflows/oblt-aw-security-triage.yml @@ -2,30 +2,25 @@ name: Security Issue Triage on: workflow_call: inputs: - ingress-event-name: - description: Relayed event name from oblt-aw-ingress - required: false + shared-proceed: + description: Dashboard gate for this route from aw-prelude. + required: true type: string - default: '' - ingress-event-action: - description: Relayed event action from oblt-aw-ingress - required: false + shared-allowed-pr-authors-json: + required: true type: string - default: '' - ingress-event-payload-json: - description: Relayed github.event JSON from oblt-aw-ingress + shared-allowed-pr-authors-csv: required: true type: string - ingress-allowed-pr-authors-csv: - description: Allowed PR bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-json: + required: true type: string - default: '' - ingress-allowed-issue-authors-csv: - description: Allowed issue bot logins from oblt-aw-ingress prelude - required: false + shared-allowed-issue-authors-csv: + required: true + type: string + shared-token-policy: + required: true type: string - default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -37,14 +32,15 @@ permissions: jobs: resolve-apm-assets: - permissions: - contents: read + if: >- + github.event_name == 'issues' && + ( + (github.event.action == 'opened' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/detector/security')) || + (github.event.action == 'labeled' && github.event.label.name == 'oblt-aw/detector/security') + ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-security-triage.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Your task is to triage newly opened security-related issues in this repository. Security issues include vulnerabilities in GitHub Actions workflows and shell scripts: injection (expression, command, YAML), secret management (token exposure via CLI args, secrets in command strings), supply chain (action pinning, checksums), and least privilege (excessive permissions). @@ -128,12 +124,17 @@ jobs: discussions: write issues: write pull-requests: write + if: >- + github.event_name == 'issues' && + ( + (github.event.action == 'opened' && contains(join(github.event.issue.labels.*.name, ','), 'oblt-aw/detector/security')) || + (github.event.action == 'labeled' && github.event.label.name == 'oblt-aw/detector/security') + ) uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-triage.lock.yml@main with: - allowed-bot-users: ${{ inputs.ingress-allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.shared-allowed-issue-authors-csv }} classification-labels: "oblt-aw/triage/security-injection,oblt-aw/triage/security-secrets,oblt-aw/triage/security-supply-chain,oblt-aw/triage/security-least-privilege,oblt-aw/triage/other,oblt-aw/triage/needs-info,oblt-aw/ai/fix-ready" additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} secrets: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} @@ -149,24 +150,26 @@ jobs: id-token: write issues: write steps: - - name: Create ephemeral GitHub token - id: create-token + - name: Create ephemeral GitHub token (configured policy) + id: create-token-explicit + if: ${{ inputs.shared-token-policy != '' }} + uses: elastic/oblt-actions/github/create-token@v1 + with: + token-policy: ${{ inputs.shared-token-policy }} + + - name: Create ephemeral GitHub token (Vault auto policy) + id: create-token-auto + if: ${{ inputs.shared-token-policy == '' }} uses: elastic/oblt-actions/github/create-token@v1 - name: Re-apply fix-ready label to emit installation-token labeled event uses: actions/github-script@v9 - env: - ISSUE_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json).issue.number }} with: - github-token: ${{ steps.create-token.outputs.token }} + github-token: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; - const issueNumber = Number(process.env.ISSUE_NUMBER); - if (!Number.isFinite(issueNumber) || issueNumber <= 0) { - core.info('No issue.number in relayed event; skipping re-apply.'); - return; - } + const issueNumber = context.issue.number; const { data: issue } = await github.rest.issues.get({ owner, repo, diff --git a/.github/workflows/oblt-aw.yml b/.github/workflows/oblt-aw.yml deleted file mode 100644 index b70c33ed..00000000 --- a/.github/workflows/oblt-aw.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Observability Agentic Workflow Entrypoint - -on: - workflow_dispatch: - inputs: - trigger-source: - description: Client trigger workflow file that dispatched this run - required: true - type: string - event-name: - description: Original github.event_name from the trigger - required: true - type: string - event-action: - description: Original github.event.action from the trigger - required: false - type: string - default: '' - event-payload-json: - description: JSON-encoded original github.event from the trigger - required: true - type: string - caller-ref: - description: Original github.ref from the trigger - required: true - type: string - caller-sha: - description: Original github.sha from the trigger - required: true - type: string - caller-run-id: - description: Original github.run_id from the trigger - required: true - type: string - -permissions: - contents: read - -concurrency: - group: oblt-aw-entrypoint-${{ github.event.inputs.event-name }}-${{ github.event.inputs.caller-run-id }} - cancel-in-progress: true - -jobs: - ingress: - permissions: - actions: read - contents: write - discussions: write - id-token: write - issues: write - pull-requests: write - uses: elastic/oblt-aw/.github/workflows/oblt-aw-ingress.yml@main # ratchet:exclude - with: - trigger-source: ${{ github.event.inputs.trigger-source }} - ingress-event-name: ${{ github.event.inputs.event-name }} - ingress-event-action: ${{ github.event.inputs.event-action }} - ingress-event-payload-json: ${{ github.event.inputs.event-payload-json }} - caller-ref: ${{ github.event.inputs.caller-ref }} - caller-sha: ${{ github.event.inputs.caller-sha }} - caller-run-id: ${{ github.event.inputs.caller-run-id }} - secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_LOGS_API_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-agent-suggestions.yml b/.github/workflows/trigger-oblt-aw-agent-suggestions.yml new file mode 100644 index 00000000..8b955e19 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-agent-suggestions.yml @@ -0,0 +1,18 @@ +name: Observability Agentic Workflow — Agent Suggestions + +on: + schedule: + - cron: "0 6 * * *" + +permissions: + contents: read + +jobs: + run-aw: + permissions: + contents: read + issues: write + pull-requests: read + uses: elastic/oblt-aw/.github/workflows/oblt-aw-agent-suggestions.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-autodoc.yml b/.github/workflows/trigger-oblt-aw-autodoc.yml new file mode 100644 index 00000000..ee48087a --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-autodoc.yml @@ -0,0 +1,19 @@ +name: Observability Agentic Workflow — Autodoc + +on: + schedule: + - cron: "0 6 * * *" + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-autodoc.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-automerge.yml b/.github/workflows/trigger-oblt-aw-automerge.yml new file mode 100644 index 00000000..a1a33bc3 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-automerge.yml @@ -0,0 +1,21 @@ +name: Observability Agentic Workflow — Automerge + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-automerge.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-dependency-review.yml b/.github/workflows/trigger-oblt-aw-dependency-review.yml new file mode 100644 index 00000000..37ce25b1 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-dependency-review.yml @@ -0,0 +1,20 @@ +name: Observability Agentic Workflow — Dependency Review + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-dependency-review.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml b/.github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml new file mode 100644 index 00000000..d9920b92 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml @@ -0,0 +1,19 @@ +name: Observability Agentic Workflow — Duplicate Issue Detector + +on: + issues: + types: [opened] + workflow_dispatch: + +permissions: + contents: read + +jobs: + run-aw: + permissions: + contents: read + issues: write + pull-requests: read + uses: elastic/oblt-aw/.github/workflows/oblt-aw-duplicate-issue-detector.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml b/.github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml new file mode 100644 index 00000000..e8986680 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml @@ -0,0 +1,22 @@ +name: Observability Agentic Workflow — PR Buildkite Detective + +on: + status: + +permissions: + contents: read + +jobs: + run-aw: + if: >- + github.event.state == 'failure' && + contains(github.event.context, 'buildkite') + permissions: + actions: read + contents: read + issues: read + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_LOGS_API_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-issue-fixer.yml b/.github/workflows/trigger-oblt-aw-issue-fixer.yml new file mode 100644 index 00000000..cf57d7bc --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-issue-fixer.yml @@ -0,0 +1,19 @@ +name: Observability Agentic Workflow — Issue Fixer + +on: + issue_comment: + types: [created] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-issue-fixer.yml@main # ratchet:exclude + secrets: inherit diff --git a/.github/workflows/trigger-oblt-aw-issue-triage.yml b/.github/workflows/trigger-oblt-aw-issue-triage.yml new file mode 100644 index 00000000..ac673945 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-issue-triage.yml @@ -0,0 +1,20 @@ +name: Observability Agentic Workflow — Issue Triage + +on: + issues: + types: [opened] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-issue-triage.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-mention-in-issue.yml b/.github/workflows/trigger-oblt-aw-mention-in-issue.yml new file mode 100644 index 00000000..17c93b99 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-mention-in-issue.yml @@ -0,0 +1,20 @@ +name: Observability Agentic Workflow — Mention in Issue + +on: + issue_comment: + types: [created] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-mention-in-issue.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml b/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml new file mode 100644 index 00000000..60bb73ed --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml @@ -0,0 +1,18 @@ +name: Observability Agentic Workflow — Resource Not Accessible Detector + +on: + schedule: + - cron: "0 6 * * *" + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + issues: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml b/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml new file mode 100644 index 00000000..111d3f8e --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml @@ -0,0 +1,19 @@ +name: Observability Agentic Workflow — Resource Not Accessible Fixer + +on: + issues: + types: [labeled] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-resource-not-accessible-by-integration-fixer.yml@main # ratchet:exclude + secrets: inherit diff --git a/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml b/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml new file mode 100644 index 00000000..191a09eb --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml @@ -0,0 +1,21 @@ +name: Observability Agentic Workflow — Resource Not Accessible Triage + +on: + issues: + types: [opened, labeled] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + discussions: write + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-resource-not-accessible-by-integration-triage.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-security-detector.yml b/.github/workflows/trigger-oblt-aw-security-detector.yml new file mode 100644 index 00000000..1265a595 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-security-detector.yml @@ -0,0 +1,19 @@ +name: Observability Agentic Workflow — Security Detector + +on: + schedule: + - cron: "0 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + id-token: write + issues: read + pull-requests: read + uses: elastic/oblt-aw/.github/workflows/oblt-aw-security-detector.yml@main # ratchet:exclude diff --git a/.github/workflows/trigger-oblt-aw-security-fixer.yml b/.github/workflows/trigger-oblt-aw-security-fixer.yml new file mode 100644 index 00000000..d87e14bd --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-security-fixer.yml @@ -0,0 +1,20 @@ +name: Observability Agentic Workflow — Security Fixer + +on: + issues: + types: [labeled] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-security-fixer.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw-security-triage.yml b/.github/workflows/trigger-oblt-aw-security-triage.yml new file mode 100644 index 00000000..b1e90ed3 --- /dev/null +++ b/.github/workflows/trigger-oblt-aw-security-triage.yml @@ -0,0 +1,21 @@ +name: Observability Agentic Workflow — Security Triage + +on: + issues: + types: [opened, labeled] + +permissions: + contents: read + +jobs: + run-aw: + permissions: + actions: read + contents: read + discussions: write + id-token: write + issues: write + pull-requests: write + uses: elastic/oblt-aw/.github/workflows/oblt-aw-security-triage.yml@main # ratchet:exclude + secrets: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} diff --git a/.github/workflows/trigger-oblt-aw.yml b/.github/workflows/trigger-oblt-aw.yml deleted file mode 100644 index 12564382..00000000 --- a/.github/workflows/trigger-oblt-aw.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Observability Agentic Workflow Trigger - -on: - schedule: - - cron: "0 6 * * *" - workflow_dispatch: - issues: - types: [opened, labeled] - issue_comment: - types: [created] - pull_request: - types: [opened, synchronize, reopened, labeled] - status: - -permissions: - contents: read - -concurrency: - group: oblt-aw-trigger-${{ github.event.pull_request.number || github.event.issue.number || github.event.schedule || github.sha }} - cancel-in-progress: true - -jobs: - dispatch-entrypoint: - permissions: - actions: write - statuses: write - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Dispatch oblt-aw entrypoint - id: dispatch - uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - workflow: oblt-aw.yml - inputs: | - { - "trigger-source": ${{ toJSON(github.workflow) }}, - "event-name": ${{ toJSON(github.event_name) }}, - "event-action": ${{ toJSON(github.event.action || '') }}, - "event-payload-json": ${{ toJSON(toJSON(github.event)) }}, - "caller-ref": ${{ toJSON(github.ref) }}, - "caller-sha": ${{ toJSON(github.sha) }}, - "caller-run-id": ${{ toJSON(github.run_id) }} - } - - - name: Post entrypoint run link on PR commit - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - RUN_URL: ${{ steps.dispatch.outputs.runUrlHtml }} - STATUS_CONTEXT: Observability Agentic Workflow Execution - run: | - gh api \ - --method POST \ - "/repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \ - -f state=success \ - -f target_url="${RUN_URL}" \ - -f description='Observability agentic workflow entrypoint dispatched (open run for progress)' \ - -f context="${STATUS_CONTEXT}" diff --git a/AGENTS.md b/AGENTS.md index ec002394..e7a7d6fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,15 +2,14 @@ ## Client entrypoint changes -Use **[`.github/remote-workflow-template/`](.github/remote-workflow-template/)** as the source for distributed client workflows (for example `obs/.github/workflows/trigger-oblt-aw.yml` and `obs/.github/workflows/oblt-aw.yml`, plus `docs/.github/workflows/trigger-docs-aw.yml` and `docs/.github/workflows/docs-aw.yml`). See [docs/workflows/oblt-aw-client-template.md](docs/workflows/oblt-aw-client-template.md), [docs/workflows/docs-aw-client-template.md](docs/workflows/docs-aw-client-template.md), and [CONTRIBUTING.md](CONTRIBUTING.md). +Use **[`.github/remote-workflow-template/`](.github/remote-workflow-template/)** as the source for distributed client workflows (per org subtree, for example `obs/.github/workflows/trigger-oblt-aw-.yml`, `docs/.github/workflows/trigger-docs-aw-*.yml`). See [docs/workflows/oblt-aw-client-template.md](docs/workflows/oblt-aw-client-template.md), [docs/workflows/docs-aw-client-template.md](docs/workflows/docs-aw-client-template.md), and [CONTRIBUTING.md](CONTRIBUTING.md). -Consumer Observability repos use **`trigger-oblt-aw.yml`** (events) → **`oblt-aw.yml`** (`workflow_dispatch`) → **`oblt-aw-ingress.yml`** (routing). - -Consumer Documentation repos use **`trigger-docs-aw.yml`** (events) → **`docs-aw.yml`** (`workflow_dispatch`) → **`docs-aw-ingress.yml`** (routing). +Do not reintroduce a monolithic `oblt-aw.yml` or `oblt-aw-ingress.yml`. ## Control-plane workflow naming - Shared prelude: `.github/workflows/aw-prelude.yml` (no org prefix). -- Observability reusables: `.github/workflows/oblt-aw-.yml` (routed via `oblt-aw-ingress.yml`, which calls `aw-prelude` once and dispatches eligible `route-*` jobs; individual `oblt-aw-*` wrappers do not call prelude). Enforced by `scripts/validate_aw_workflow_prelude.py` in CI. Workflows that invoke `gh-aw-*` must call `aw-resolve-apm-assets.yml` per agent job (`scripts/validate_aw_workflow_resolve_apm_assets.py`). -- Docs reusables: `.github/workflows/docs-aw-*.yml` (routed via `docs-aw-ingress.yml`; same ingress/prelude model as Observability; same `aw-resolve-apm-assets` requirement for `gh-aw-*` jobs). +- Observability route reusables: `.github/workflows/oblt-aw-.yml` (declare `shared-proceed`; prelude runs in `oblt-aw-event-*` orchestrators). Workflows that invoke `gh-aw-*` must call `aw-resolve-apm-assets.yml` per agent job (`scripts/validate_aw_workflow_resolve_apm_assets.py`). +- Docs route reusables: `.github/workflows/docs-aw-*.yml` (same `shared-proceed` contract; prelude runs in `docs-aw-event-*` orchestrators). +- Enforced by `scripts/validate_aw_workflow_prelude.py` in CI. - Upstream lock files in `elastic/ai-github-actions` / `elastic/docs-actions` keep the `gh-aw-*` prefix. diff --git a/README.md b/README.md index c564c60b..7320f2bd 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,10 @@ See [docs/development/contributing.md](docs/development/contributing.md) for ful ## Quick Start -Target repositories install per-workflow client templates from this repository (for example `trigger-oblt-aw-automerge.yml`, `oblt-aw-issue-fixer.yml`). Each template calls a matching `oblt-aw-*` reusable workflow; shared gating runs in [aw-prelude.yml](.github/workflows/aw-prelude.yml). +Target repositories install event-scoped client templates from this repository (for example `trigger-oblt-aw-pull-request.yml`, `trigger-oblt-aw-issues.yml`). Each event client calls an `oblt-aw-event-*` orchestrator that runs shared dashboard gating via [aw-prelude.yml](.github/workflows/aw-prelude.yml) and passes `shared-proceed` (plus allow-list fields) into each route reusable. - Observability templates: [.github/remote-workflow-template/obs/.github/workflows/](.github/remote-workflow-template/obs/.github/workflows/) — see [docs/workflows/oblt-aw-client-template.md](docs/workflows/oblt-aw-client-template.md) -- Docs templates: [.github/remote-workflow-template/docs/.github/workflows/](.github/remote-workflow-template/docs/.github/workflows/) (`trigger-docs-aw.yml`, `docs-aw.yml`) +- Docs templates: [.github/remote-workflow-template/docs/.github/workflows/](.github/remote-workflow-template/docs/.github/workflows/) (`trigger-docs-aw-issues.yml`, `trigger-docs-aw-issue-comment.yml`, `trigger-docs-aw-pull-request.yml`, `trigger-docs-aw-workflow-run.yml`) ## Repository Scope diff --git a/config/docs/active-repositories.json b/config/docs/active-repositories.json index 855ab13e..fc04f304 100644 --- a/config/docs/active-repositories.json +++ b/config/docs/active-repositories.json @@ -1,5 +1,8 @@ { "repositories": [ - "elastic/opentelemetry" + { + "repository": "elastic/opentelemetry", + "token-policy": "token-policy-6f4d825f492c" + } ] } diff --git a/config/docs/workflow-registry.json b/config/docs/workflow-registry.json index b87d0f23..bf43aa28 100644 --- a/config/docs/workflow-registry.json +++ b/config/docs/workflow-registry.json @@ -1,4 +1,5 @@ { + "section_title": "Documentation", "workflows": [ { "id": "docs-issue-ai-menu", @@ -6,10 +7,8 @@ "description": "Adds a documentation issue AI menu.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "ai-menu" - } + "control_plane_workflows": [ + "docs-aw-ai-menu.yml" ] }, { @@ -18,13 +17,9 @@ "description": "Adds a documentation PR AI menu.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "pr-ai-menu-collect" - }, - { - "id": "pr-ai-menu" - } + "control_plane_workflows": [ + "docs-aw-pr-ai-menu-collect.yml", + "docs-aw-pr-ai-menu.yml" ] } ] diff --git a/config/obs/active-repositories.json b/config/obs/active-repositories.json index 7d34f460..05ade9d4 100644 --- a/config/obs/active-repositories.json +++ b/config/obs/active-repositories.json @@ -1,15 +1,48 @@ { "repositories": [ - "elastic/gh-oblt", - "elastic/oblt-actions", - "elastic/oblt-aw", - "elastic/oblt-cli", - "elastic/oblt-framework", - "elastic/oblt-infra", - "elastic/oblt-robot", - "elastic/observability-catalog", - "elastic/observability-github-secrets", - "elastic/observability-github-settings", - "elastic/observability-robots" + { + "repository": "elastic/gh-oblt", + "token-policy": "token-policy-bea69d21b520" + }, + { + "repository": "elastic/oblt-actions", + "token-policy": "token-policy-dbe14d22fc0d" + }, + { + "repository": "elastic/oblt-aw", + "token-policy": "token-policy-267984e2662f" + }, + { + "repository": "elastic/oblt-cli", + "token-policy": "token-policy-36401b7c6eba" + }, + { + "repository": "elastic/oblt-framework", + "token-policy": "token-policy-371e815a5a93" + }, + { + "repository": "elastic/oblt-infra", + "token-policy": "token-policy-65f0f52af128" + }, + { + "repository": "elastic/oblt-robot", + "token-policy": "token-policy-e2201d02661f" + }, + { + "repository": "elastic/observability-catalog", + "token-policy": "" + }, + { + "repository": "elastic/observability-github-secrets", + "token-policy": "token-policy-1a20b562a03f" + }, + { + "repository": "elastic/observability-github-settings", + "token-policy": "token-policy-b15759d51a6a" + }, + { + "repository": "elastic/observability-robots", + "token-policy": "token-policy-711a6d8c9026" + } ] } diff --git a/config/obs/automerge-dependency-collections.json b/config/obs/automerge-dependency-collections.json index f12ca136..24082c8e 100644 --- a/config/obs/automerge-dependency-collections.json +++ b/config/obs/automerge-dependency-collections.json @@ -53,21 +53,6 @@ "**/pnpm-lock.yaml" ] }, - { - "id": "terraform", - "description": "Terraform/OpenTofu/Terragrunt dependency and lockfile updates", - "active": true, - "file-glob": [ - "**/*.tf", - "**/*.tfvars", - "**/*.tfvars.json", - "**/.terraform.lock.hcl", - "**/terragrunt.hcl", - "**/.opentofu-version", - "**/.terraform-version", - "**/.terragrunt-version" - ] - }, { "id": "vm-images", "description": "CI runner or container image pin updates", diff --git a/config/obs/workflow-registry.json b/config/obs/workflow-registry.json index 8083fca1..2e2d7bbb 100644 --- a/config/obs/workflow-registry.json +++ b/config/obs/workflow-registry.json @@ -1,4 +1,5 @@ { + "section_title": "Observability", "workflows": [ { "id": "agent-suggestions", @@ -6,11 +7,7 @@ "description": "Suggests agentic workflows and improvements for the repository based on analysis of current setup.", "maturity": "early-adoption", "default_enabled": true, - "ingress_routes": [ - { - "id": "agent-suggestions" - } - ] + "control_plane_workflows": ["oblt-aw-agent-suggestions.yml"] }, { "id": "autodoc", @@ -18,11 +15,7 @@ "description": "Analyzes documentation for gaps, outdated content, and inconsistencies; creates issues and PRs to improve docs.", "maturity": "early-adoption", "default_enabled": true, - "ingress_routes": [ - { - "id": "autodoc" - } - ] + "control_plane_workflows": ["oblt-aw-autodoc.yml"] }, { "id": "automerge", @@ -30,12 +23,7 @@ "description": "Enables auto-merge for allowed bot PRs (same authors as dependency-review) with oblt-aw/ai/merge-ready when validation and approval gates pass; GitHub enforces required checks at merge time.", "maturity": "early-adoption", "default_enabled": true, - "ingress_routes": [ - { - "id": "automerge", - "allowed_bot_users_from": "allowed-pr" - } - ] + "control_plane_workflows": ["oblt-aw-automerge.yml"] }, { "id": "dependency-review", @@ -43,12 +31,7 @@ "description": "Analyzes dependency-update PRs from bots, applies merge-ready labels when criteria are met.", "maturity": "early-adoption", "default_enabled": true, - "ingress_routes": [ - { - "id": "dependency-review", - "allowed_bot_users_from": "allowed-pr" - } - ] + "control_plane_workflows": ["oblt-aw-dependency-review.yml"] }, { "id": "duplicate-issue-detector", @@ -56,11 +39,7 @@ "description": "Detects potential duplicate issues when a new issue is opened or when the entrypoint is run manually.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "duplicate-issue-detector" - } - ] + "control_plane_workflows": ["oblt-aw-duplicate-issue-detector.yml"] }, { "id": "issue-triage", @@ -68,11 +47,7 @@ "description": "Triages newly opened issues using the generic issue-triage agentic workflow.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "issue-triage" - } - ] + "control_plane_workflows": ["oblt-aw-issue-triage.yml"] }, { "id": "issue-fixer", @@ -80,11 +55,7 @@ "description": "Executes generic issue fixes requested with /ai implement comments, excluding specialized security and resource-not-accessible flows.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "issue-fixer" - } - ] + "control_plane_workflows": ["oblt-aw-issue-fixer.yml"] }, { "id": "mention-in-issue", @@ -92,11 +63,7 @@ "description": "AI assistant for issues — answers questions, debugs problems, and creates PRs on demand when triggered with a /ai comment.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "mention-in-issue" - } - ] + "control_plane_workflows": ["oblt-aw-mention-in-issue.yml"] }, { "id": "security", @@ -104,18 +71,10 @@ "description": "Runs static security checks on workflows, shell scripts, and dependency manifests per the security scanning ruleset; opens issues for findings.", "maturity": "experimental", "default_enabled": false, - "ingress_routes": [ - { - "id": "security-detector" - }, - { - "id": "security-fixer", - "allowed_bot_users_from": "allowed-issue" - }, - { - "id": "security-triage", - "allowed_bot_users_from": "allowed-issue" - } + "control_plane_workflows": [ + "oblt-aw-security-detector.yml", + "oblt-aw-security-fixer.yml", + "oblt-aw-security-triage.yml" ] }, { @@ -124,18 +83,10 @@ "description": "Detects 'Resource not accessible by integration' occurrences in workflow logs, creates triage issues, triages newly opened issues, and executes fixes for issues labeled ready to fix.", "maturity": "early-adoption", "default_enabled": false, - "ingress_routes": [ - { - "id": "resource-not-accessible-by-integration-detector" - }, - { - "id": "resource-not-accessible-by-integration-fixer", - "allowed_bot_users_from": "allowed-issue" - }, - { - "id": "resource-not-accessible-by-integration-triage", - "allowed_bot_users_from": "allowed-issue" - } + "control_plane_workflows": [ + "oblt-aw-resource-not-accessible-by-integration-detector.yml", + "oblt-aw-resource-not-accessible-by-integration-fixer.yml", + "oblt-aw-resource-not-accessible-by-integration-triage.yml" ] }, { @@ -144,11 +95,7 @@ "description": "Analyzes Buildkite CI failures for a PR when a Buildkite status check fails; posts a diagnostic comment on the pull request.", "maturity": "early-adoption", "default_enabled": false, - "ingress_routes": [ - { - "id": "estc-pr-buildkite-detective" - } - ] + "control_plane_workflows": ["oblt-aw-estc-pr-buildkite-detective.yml"] } ] } diff --git a/docs/onboarding/adopting-agentic-workflows.md b/docs/onboarding/adopting-agentic-workflows.md index 26584b59..f70defff 100644 --- a/docs/onboarding/adopting-agentic-workflows.md +++ b/docs/onboarding/adopting-agentic-workflows.md @@ -22,10 +22,10 @@ Each **organization** owns `config//` (for example `config/obs/`): [`wo - Add `.github/workflows/trigger-oblt-aw-.yml (client); oblt-aw-.yml` at the repository root. - When the agent graph lives in **`elastic/ai-github-actions`**, add a thin wrapper that calls the pinned lock file and pass domain-specific `with:` / `secrets:`. -### 2. Add prelude and route conditions +### 2. Add route contract and event orchestration -- Do **not** call `aw-prelude.yml` from `oblt-aw-*` / `docs-aw-*` wrappers; add an `ingress_routes` entry and a matching `route-*` job in `oblt-aw-ingress.yml` or `docs-aw-ingress.yml` instead. -- Downstream jobs: `needs: prelude` and `if: needs.prelude.outputs.proceed == 'true'` plus event/label/comment guards ([aw-prelude](../workflows/aw-prelude.md)). +- Route reusable (`oblt-aw-*` / `docs-aw-*`): declare required `shared-proceed` (and shared allow-list / token-policy inputs); gate agent jobs with `if: inputs.shared-proceed == 'true'` plus event/label/comment guards. Do **not** call `aw-prelude.yml` from route workflows. +- Event orchestrator (`*-aw-event-*.yml`): first job calls [aw-prelude.yml](../workflows/aw-prelude.md) with `control-plane-workflows` listing every route basename for that GitHub event family; fan out with `fromJSON(needs.prelude.outputs.proceed-by-workflow)['']` ([aw-prelude](../workflows/aw-prelude.md)). ### 3. Mirror permissions from similar workflows @@ -37,7 +37,7 @@ Each **organization** owns `config//` (for example `config/obs/`): [`wo ### 5. Register in `workflow-registry.json` -- Add one object with unique `id`, `name`, `description`, `maturity`, `default_enabled`, and `ingress_routes` (one entry per route leg that shares this dashboard id) under `config//workflow-registry.json`. +- Add one object with unique `id`, `name`, `description`, `maturity`, `default_enabled`, and `control_plane_workflows` (basenames of every `oblt-aw-*` / `docs-aw-*` wrapper that share this dashboard id) under `config//workflow-registry.json`. ### 6. Add a client template @@ -68,7 +68,7 @@ Each **organization** owns `config//` (for example `config/obs/`): [`wo ## Troubleshooting - **Workflow never runs after checking the box** — Wait for a supported trigger on the installed `trigger-oblt-aw-*.yml` client ([oblt-aw-client-template](../workflows/oblt-aw-client-template.md)). -- **Validation fails on the PR** — Compare `permissions` with a sibling wrapper; confirm the wrapper is listed in `ingress_routes` and that a matching `route-*` job exists in the org ingress workflow. +- **Validation fails on the PR** — Compare `permissions` with a sibling wrapper; confirm the route basename is listed under the correct `control_plane_workflows` entry in `workflow-registry.json` and appears in the matching event orchestrator’s `control-plane-workflows` input. ## References diff --git a/docs/onboarding/registering-a-repository.md b/docs/onboarding/registering-a-repository.md index bb1d12d2..16fd1adb 100644 --- a/docs/onboarding/registering-a-repository.md +++ b/docs/onboarding/registering-a-repository.md @@ -20,12 +20,15 @@ Consumer repositories in this guide are always under the **`elastic`** GitHub or ## Steps -1. **Create a new branch on `elastic/oblt-aw`, choose the org, and add the repository** — From an up-to-date `main`, create a **feature branch** (for example `git checkout -b feat/oblt-aw-register-`; exact naming is your team’s convention). On that branch only, identify the owning **`config//`** folder ([multi-org design](../architecture/multi-org-agentic-workflows.md)) and edit **`active-repositories.json`** to add **`elastic/`** ([distribute-client-workflow](../operations/distribute-client-workflow.md)): +1. **Create a new branch on `elastic/oblt-aw`, choose the org, and add the repository** — From an up-to-date `main`, create a **feature branch** (for example `git checkout -b feat/oblt-aw-register-`; exact naming is your team’s convention). On that branch only, identify the owning **`config//`** folder ([multi-org design](../architecture/multi-org-agentic-workflows.md)) and edit **`active-repositories.json`** to add an object with **`repository`** set to **`elastic/`** and **`token-policy`** set to a Backstage policy name when this repo should use an explicit policy for all `create-token` calls, or **`""`** otherwise ([distribute-client-workflow](../operations/distribute-client-workflow.md)): ```json { "repositories": [ - "elastic/" + { + "repository": "elastic/", + "token-policy": "" + } ] } ``` @@ -137,8 +140,8 @@ Draft placeholder for `additional_permissions` (not valid YAML until substituted |--------------------|-------------------------------------------------------| | [distribute-client-workflow.yml](../../.github/workflows/distribute-client-workflow.yml) | `token-policy-63405ab45244` | | [sync-control-plane-dashboard.yml](../../.github/workflows/sync-control-plane-dashboard.yml) | `token-policy-8b60ba56dd3f` | -| [oblt-aw-security-detector.yml](../../.github/workflows/oblt-aw-security-detector.yml) | Vault auto policy (no `with.token-policy` on `create-token`) | -| [oblt-aw-automerge.yml](../../.github/workflows/oblt-aw-automerge.yml) (ephemeral token step) | Vault auto policy (no `with.token-policy` on `create-token`) | +| [oblt-aw-security-detector.yml](../../.github/workflows/oblt-aw-security-detector.yml) | `-` (no explicit `with.token-policy` on `create-token`) | +| [oblt-aw-automerge.yml](../../.github/workflows/oblt-aw-automerge.yml) (ephemeral token step) | `-` (no explicit `with.token-policy` on `create-token`) | ## Troubleshooting diff --git a/docs/operations/control-plane-dashboard-format.md b/docs/operations/control-plane-dashboard-format.md index 057175a0..5abe400b 100644 --- a/docs/operations/control-plane-dashboard-format.md +++ b/docs/operations/control-plane-dashboard-format.md @@ -22,7 +22,7 @@ There is no config file (no `.github/oblt-aw-config.json`), no PRs when users to Every workflow in ingress gating is identified by **`org-key:workflow-id`**: - **`org-key`** — Directory name under `config//` in `elastic/oblt-aw` (for example `obs`, `docs`). Not to be confused with the repository’s root `docs/` Markdown tree. -- **`workflow-id`** — Unique within that org’s [`workflow-registry.json`](../../config/obs/workflow-registry.json). Each registry entry lists every control-plane reusable it gates via `ingress_routes` (basenames under `.github/workflows/`, for example `oblt-aw-security-detector.yml`); multiple files may share one `id` (detector/fixer/triage). +- **`workflow-id`** — Unique within that org’s [`workflow-registry.json`](../../config/obs/workflow-registry.json). Each registry entry lists every control-plane reusable it gates via `control_plane_workflows` (basenames under `.github/workflows/`, for example `oblt-aw-security-detector.yml`); multiple files may share one `id` (detector/fixer/triage). **Examples:** `obs:agent-suggestions`, `docs:example-workflow`. diff --git a/docs/operations/distribute-client-workflow.md b/docs/operations/distribute-client-workflow.md index 7a05e2c5..345f5b22 100644 --- a/docs/operations/distribute-client-workflow.md +++ b/docs/operations/distribute-client-workflow.md @@ -10,7 +10,7 @@ This workflow distributes or removes client files from each org’s subtree unde - Per-org [active-repositories.json](../../config/obs/active-repositories.json) files under `config//` list current target repositories (union used for distribution). - Per-org templates under [.github/remote-workflow-template//](../../.github/remote-workflow-template/) are the **only** sources for files installed into consumer repositories (for example `obs/.github/workflows/trigger-oblt-aw-*.yml` → `.github/workflows/trigger-oblt-aw-*.yml`). Edit only under [remote-workflow-template](../../.github/remote-workflow-template/) (see [Client template doc](../workflows/oblt-aw-client-template.md)). -- Control-plane token policy for [elastic/oblt-actions/github/create-token@v1](https://github.com/elastic/oblt-actions/tree/v1/github/create-token) is set in [distribute-client-workflow.yml](../../.github/workflows/distribute-client-workflow.yml) (`token-policy-63405ab45244`), not in `active-repositories.json`. +- Token policy configured for [elastic/oblt-actions/github/create-token@v1](https://github.com/elastic/oblt-actions/tree/v1/github/create-token). ## Usage @@ -34,8 +34,14 @@ Execution stages: ```json { "repositories": [ - "elastic/oblt-aw", - "elastic/oblt-cli" + { + "repository": "elastic/oblt-aw", + "token-policy": "" + }, + { + "repository": "elastic/oblt-cli", + "token-policy": "token-policy-abc123def456" + } ] } ``` @@ -43,15 +49,15 @@ Execution stages: Validation and normalization rules: - `repositories` must resolve to a JSON list. -- Each entry is either an `owner/repo` string or an object with required `repository` (`owner/repo`). -- Consumer `create-token` steps use Vault auto policy per trigger workflow ref (no policy in config). Control-plane `distribute-client-workflow` and `sync-control-plane-dashboard` use fixed workflow token policies in their YAML (`token-policy-63405ab45244` and `token-policy-8b60ba56dd3f`). +- Every entry is an object with required `repository` (`owner/repo`) and `token-policy` (string; use `""` when Vault auto policy / control-plane defaults apply). +- When `token-policy` is non-empty, consumer `create-token` steps (via `aw-prelude`) use that policy for that repository; when empty, consumer workflows keep Vault auto policy per trigger workflow ref. Control-plane `distribute-client-workflow` and `sync-control-plane-dashboard` always use their fixed workflow token policies (`token-policy-63405ab45244` and `token-policy-8b60ba56dd3f`). - Entries are normalized (trimmed), de-duplicated, and sorted before processing. -- Invalid entries fail the step with: `Invalid repository entry: ... Expected 'owner/repo' string or object with 'repository'`. +- Invalid entries fail the step with: `Invalid repository entry: ... Expected object with 'repository'`. Examples: -- Valid: `"elastic/oblt-aw"`, `{"repository": "elastic/oblt-aw"}` -- Invalid: `"elastic"` (missing slash), `123` (non-string/non-object), `{"repo":"elastic/oblt-aw"}` (wrong key) +- Valid: `{"repository": "elastic/oblt-aw", "token-policy": ""}` +- Invalid: `"elastic/oblt-aw"` (bare string), `"elastic"` (missing slash in `repository`), `123` (non-object), `{"repo":"elastic/oblt-aw"}` (wrong key) ## `build_target_operations.py` Contract diff --git a/docs/workflows/README.md b/docs/workflows/README.md index c1e5a0c0..725ca9a1 100644 --- a/docs/workflows/README.md +++ b/docs/workflows/README.md @@ -11,8 +11,7 @@ This section provides documentation for each workflow source in [.github/workflo - Dashboard reader (reusable workflow): [docs/workflows/get-enabled-workflows.md](get-enabled-workflows.md) - PR and issue allow-list loader (reusable workflow): [docs/workflows/load-allowed-authors.md](load-allowed-authors.md) - Observability client templates (`trigger-oblt-aw-*.yml` under remote-workflow-template): [docs/workflows/oblt-aw-client-template.md](oblt-aw-client-template.md) -- Docs client templates (`trigger-docs-aw.yml`, `docs-aw.yml`): [docs/workflows/docs-aw-client-template.md](docs-aw-client-template.md) -- Docs ingress: [docs/workflows/docs-aw-ingress.md](docs-aw-ingress.md) +- Docs client templates (`trigger-docs-aw-*.yml` under remote-workflow-template): [docs/workflows/docs-aw-client-template.md](docs-aw-client-template.md) - Docs issue AI menu reusable workflow: [docs/workflows/docs-aw-ai-menu.md](docs-aw-ai-menu.md) - Docs PR AI menu reusable workflow: [docs/workflows/docs-aw-pr-ai-menu.md](docs-aw-pr-ai-menu.md) - Agent suggestions workflow: [docs/workflows/oblt-aw-agent-suggestions.md](oblt-aw-agent-suggestions.md) diff --git a/docs/workflows/aw-prelude.md b/docs/workflows/aw-prelude.md index 91477272..74f6f48b 100644 --- a/docs/workflows/aw-prelude.md +++ b/docs/workflows/aw-prelude.md @@ -4,9 +4,11 @@ Source file: [.github/workflows/aw-prelude.yml](../../.github/workflows/aw-prelude.yml) -Shared reusable prelude for agentic workflows (dashboard read, optional allow lists). +Shared reusable prelude for agentic workflows (dashboard gating and optional allow lists). -`oblt-aw-ingress.yml` and `docs-aw-ingress.yml` each invoke this prelude once per run. Individual `oblt-aw-*` and `docs-aw-*` wrappers do **not** call prelude; route jobs in ingress gate on `enabled-workflows` and relay event context. CI enforces this via [scripts/validate_aw_workflow_prelude.py](../../scripts/validate_aw_workflow_prelude.py). +Event-scoped orchestrators (`oblt-aw-event-*`, `docs-aw-event-*`) call this workflow once per GitHub event family, then fan out to per-route `*-aw-*` workflows with `shared-proceed` and related outputs. CI enforces that route reusables declare `shared-proceed` via [scripts/validate_aw_workflow_prelude.py](../../scripts/validate_aw_workflow_prelude.py). + +APM asset resolution (`apm install`, `apm.yml` merge) is **not** part of the prelude. Call [aw-resolve-apm-assets.yml](aw-resolve-apm-assets.md) once per `gh-aw-*` agent invocation instead. ## Contract @@ -14,25 +16,29 @@ Shared reusable prelude for agentic workflows (dashboard read, optional allow li | Input | Type | Default | Purpose | |-------|------|---------|---------| +| `control-plane-workflows` | string | (required) | JSON array of control-plane workflow basenames to evaluate (for example `["oblt-aw-automerge.yml","oblt-aw-dependency-review.yml"]`) | | `load-allowed-authors` | boolean | `false` | When true, loads PR and issue bot allow lists on `pull_request` / `issues` events | -| `ingress-event-name` | string | `''` | Relayed event name from ingress (empty uses `github.event_name`) | ### Outputs | Output | Description | |--------|-------------| -| `proceed` | Always `true` (dashboard gating is enforced per route in ingress) | +| `proceed-by-workflow` | JSON map of workflow basename → `true`/`false` proceed flags | | `effective-raw` | Raw dashboard read (`''` means all workflows enabled) | | `enabled-workflows` | Normalized JSON array of compound ids | | `allowed-pr-authors-json` / `allowed-pr-authors-csv` | PR allow list (empty when not loaded) | | `allowed-issue-authors-json` / `allowed-issue-authors-csv` | Issue allow list (empty when not loaded) | +| `token-policy` | Repository token policy when configured | + +### Gating rule -Consumer workflows that call `create-token` rely on Vault auto policy per trigger `workflow_ref` (no prelude output). +- `effective-raw` empty → all listed routes proceed +- Otherwise each route proceeds only when its registry-resolved compound id is in `enabled-workflows` ## References - [get-enabled-workflows.md](get-enabled-workflows.md) - [load-allowed-authors.md](load-allowed-authors.md) - [aw-resolve-apm-assets.md](aw-resolve-apm-assets.md) -- [oblt-aw-ingress.md](oblt-aw-ingress.md) -- [docs-aw-ingress.md](docs-aw-ingress.md) +- [oblt-aw-client-template.md](oblt-aw-client-template.md) +- [docs-aw-client-template.md](docs-aw-client-template.md) diff --git a/docs/workflows/aw-resolve-apm-assets.md b/docs/workflows/aw-resolve-apm-assets.md index 30ca591a..14298d9c 100644 --- a/docs/workflows/aw-resolve-apm-assets.md +++ b/docs/workflows/aw-resolve-apm-assets.md @@ -20,9 +20,6 @@ Wrappers that only gate or run scripts (for example `oblt-aw-security-detector.y | `platform-additional-instructions` | string | `""` | Control-plane baseline text for this agent invocation (prepended before repo APM instructions) | | `platform-inputs-json` | string | `"{}"` | JSON object of platform inputs; APM `inputs` override per key | | `install-apm-packages` | boolean | `true` | Run `apm install` when `apm.yml` is present | -| `ingress-event-name` | string | `""` | Relayed `github.event_name` from ingress (prepended gh-aw context block) | -| `ingress-event-action` | string | `""` | Relayed `github.event.action` from ingress | -| `ingress-event-payload-json` | string | `""` | Relayed `github.event` JSON from ingress; drives authoritative PR/issue numbers in agent prompts and optional PR checkout setup commands | ### Outputs @@ -31,22 +28,31 @@ Wrappers that only gate or run scripts (for example `oblt-aw-security-detector.y | `apm-manifest-present` | Consumer has `apm.yml` / `apm.yaml` | | `apm-extension-present` | Manifest contains `x-oblt-aw` | | `asset-source` | `none`, `common`, or `workflow` | -| `resolved-additional-instructions` | Relayed ingress context (when present), then merged platform + APM instructions | +| `resolved-additional-instructions` | Merged platform + APM instructions | | `resolved-inputs-json` | Merged platform + APM inputs | -| `resolved-setup-commands-json` | Relayed PR checkout commands (when applicable), then APM `setup-commands` | +| `resolved-setup-commands-json` | JSON array of shell commands from the selected asset block (`setup-commands` inline string/list and optional `setup-commands-file`) | -### Relayed GitHub context +### Typical caller pattern -Consumer entrypoints relay the original webhook payload through ingress (`ingress-event-payload-json`). Upstream `gh-aw-*` lock files still read native `github.event`, which is empty under the `workflow_dispatch` entrypoint model. This workflow injects an authoritative **Relayed ingress context** block at the top of `resolved-additional-instructions` and, for pull requests, prepends authenticated PR checkout setup commands (`gh auth setup-git`, then `git fetch pull/N/head` and `git checkout -B FETCH_HEAD`) to `resolved-setup-commands-json`. Fetching into the checked-out branch ref fails in CI when the runner already has the PR branch checked out; `checkout -B` resets safely from `FETCH_HEAD`. Callers should pass `setup-commands: ${{ join(fromJSON(needs..outputs.resolved-setup-commands-json), ' && ') }}` to `gh-aw-*` jobs that accept the input. +Place each `resolve-apm-assets` job **immediately before** the `gh-aw-*` job it feeds, after any prerequisite gates (verify, discover, evaluate-trigger, etc.). Use the **same `if` expression** on resolve and agent so APM install/resolution runs only when the agent will. ```yaml +jobs: + prelude: + uses: ./.github/workflows/aw-prelude.yml + with: + control-plane-workflow: oblt-aw-example.yml + + # ... optional intermediate jobs (verify, discover, menu scripts, etc.) ... + resolve-apm-assets: + needs: [prelude] # plus any jobs the agent also needs + if: >- + needs.prelude.outputs.proceed == 'true' && + uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-example.yml - ingress-event-name: ${{ inputs.ingress-event-name }} - ingress-event-action: ${{ inputs.ingress-event-action }} - ingress-event-payload-json: ${{ inputs.ingress-event-payload-json }} platform-additional-instructions: | Platform prompt for this agent invocation. @@ -58,11 +64,8 @@ Consumer entrypoints relay the original webhook payload through ingress (`ingres uses: elastic/ai-github-actions/.github/workflows/gh-aw-example.lock.yml@main with: additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} - setup-commands: ${{ join(fromJSON(needs.resolve-apm-assets.outputs.resolved-setup-commands-json), ' && ') }} ``` -Place each `resolve-apm-assets` job **immediately before** the `gh-aw-*` job it feeds, after any prerequisite gates (verify, discover, evaluate-trigger, etc.). Use the **same `if` expression** on resolve and agent so APM install/resolution runs only when the agent will. - Examples with upstream gates: [oblt-aw-automerge.yml](../../.github/workflows/oblt-aw-automerge.yml) (resolve after verify + dependency collection), [oblt-aw-resource-not-accessible-by-integration-detector.yml](../../.github/workflows/oblt-aw-resource-not-accessible-by-integration-detector.yml) (resolve after discover). Multi-agent wrappers use one resolve job per `gh-aw-*` invocation — see [oblt-aw-autodoc.yml](../../.github/workflows/oblt-aw-autodoc.yml). ## References @@ -71,4 +74,3 @@ Examples with upstream gates: [oblt-aw-automerge.yml](../../.github/workflows/ob - [APM agentic assets architecture](../architecture/apm-agentic-assets.md) - [Agentic Workflow Prelude](aw-prelude.md) - [scripts/resolve_apm_agentic_assets.py](../../scripts/resolve_apm_agentic_assets.py) -- [scripts/ingress_github_context.py](../../scripts/ingress_github_context.py) diff --git a/docs/workflows/distribute-client-workflow.md b/docs/workflows/distribute-client-workflow.md index db4cded9..23d5a04e 100644 --- a/docs/workflows/distribute-client-workflow.md +++ b/docs/workflows/distribute-client-workflow.md @@ -33,7 +33,7 @@ Core behavior: ### Input and output contracts -- Target config input is an object with `repositories: ["owner/repo", ...]` (or legacy objects with `repository` only) +- Target config input is an object with `repositories: [{ "repository": "owner/repo", "token-policy": "" }, ...]` - The target builder step exposes: - `targets` (JSON matrix entries with `repository`, `operation`, `files`, and for install ops `remove_files`) - `has_targets` (`true`/`false`) diff --git a/docs/workflows/docs-aw-ai-menu.md b/docs/workflows/docs-aw-ai-menu.md index e971f06d..6bac3109 100644 --- a/docs/workflows/docs-aw-ai-menu.md +++ b/docs/workflows/docs-aw-ai-menu.md @@ -4,11 +4,11 @@ Source file: [.github/workflows/docs-aw-ai-menu.yml](../../.github/workflows/docs-aw-ai-menu.yml) -Reusable implementation for the Docs issue AI menu. Routed from `docs-aw-ingress.yml` when the client `trigger-docs-aw.yml` relays a matching event. +Reusable implementation for the Docs issue AI menu. Event-scoped client templates call `docs-aw-event-issues.yml` or `docs-aw-event-issue-comment.yml`, which fan out to this route on supported events. ## Prerequisites -- Triggered via `workflow_call` from [docs-aw-ingress.yml](../../.github/workflows/docs-aw-ingress.yml) (`route-ai-menu`). +- Triggered via `workflow_call` from [docs-aw-event-issues.yml](../../.github/workflows/docs-aw-event-issues.yml) or [docs-aw-event-issue-comment.yml](../../.github/workflows/docs-aw-event-issue-comment.yml) after client templates in [docs-aw-client-template.md](docs-aw-client-template.md). - Optional secret input: `COPILOT_GITHUB_TOKEN` (`required: false` at this workflow boundary). ## Usage diff --git a/docs/workflows/docs-aw-client-template.md b/docs/workflows/docs-aw-client-template.md index 91fc6e8f..d62c9f37 100644 --- a/docs/workflows/docs-aw-client-template.md +++ b/docs/workflows/docs-aw-client-template.md @@ -1,59 +1,78 @@ -# Workflow: Client templates `trigger-docs-aw.yml` and `docs-aw.yml` +# Workflow: Client templates `trigger-docs-aw-*.yml` ## Overview -Consumer Documentation repositories install workflows from [`.github/remote-workflow-template/docs/.github/workflows/`](../../.github/remote-workflow-template/docs/.github/workflows/) via [distribute-client-workflow](distribute-client-workflow.md). +**Source of truth (edit here only):** [.github/remote-workflow-template/docs/.github/workflows/](../../.github/remote-workflow-template/docs/.github/workflows/) -Flow: **`trigger-docs-aw.yml`** (events) → **`docs-aw.yml`** (`workflow_dispatch`) → **`docs-aw-ingress.yml`** (routing). +`distribute-client-workflow` installs these files into consumer repositories for every repository listed under [config/docs/active-repositories.json](../../config/docs/active-repositories.json). -Control-plane reusables are referenced from `elastic/oblt-aw`: +## Event-scoped client model + +Client templates are grouped by **GitHub event family** so co-triggered routes share one dashboard read per workflow run. Each event-scoped client calls an orchestrator reusable (`docs-aw-event-*.yml`) that runs [aw-prelude.yml](aw-prelude.md) once, then fans out to per-route `docs-aw-*` workflows. ```yaml -uses: elastic/oblt-aw/.github/workflows/docs-aw-ingress.yml@main +uses: elastic/oblt-aw/.github/workflows/docs-aw-event-pull-request.yml@main ``` -Shared dashboard gating and prelude run in ingress, not in the client trigger file. +Per-route dashboard gating uses the required `shared-proceed` input (and related shared allow-list fields) passed from [aw-prelude.yml](aw-prelude.md) via each `docs-aw-event-*` orchestrator. + +### Template index + +| Client template | Triggers | Orchestrator → routes | +|-----------------|----------|------------------------| +| `trigger-docs-aw-issues.yml` | `issues` opened; `workflow_dispatch` (`issue_number` required) | `docs-aw-event-issues.yml` → `docs-aw-ai-menu.yml` | +| `trigger-docs-aw-issue-comment.yml` | `issue_comment` edited | `docs-aw-event-issue-comment.yml` → `docs-aw-ai-menu.yml`, `docs-aw-pr-ai-menu.yml` | +| `trigger-docs-aw-pull-request.yml` | `pull_request` (opened, reopened, synchronize, ready_for_review) | `docs-aw-event-pull-request.yml` → `docs-aw-pr-ai-menu-collect.yml` | +| `trigger-docs-aw-workflow-run.yml` | `workflow_run` on collect workflow (completed); `workflow_dispatch` (`pull_request_number` required) | `docs-aw-event-workflow-run.yml` → `docs-aw-pr-ai-menu.yml` | + +Route-specific conditions (for example PR vs non-PR issue comments, menu checkbox transitions) are enforced inside each `docs-aw-*` reusable workflow after prelude gating. + +### Fork PRs and SEC-043 (split-workflow) + +Fork PRs cannot post issue comments with a write-capable `GITHUB_TOKEN` from a `pull_request` workflow. `pull_request_target` is unsafe (runs in the base repo with elevated token on untrusted fork events). The PR menu therefore uses a **split-workflow** pattern: -## Installed client workflows +1. **`trigger-docs-aw-pull-request.yml`** — `pull_request` only; uploads a `pr-number` artifact via [docs-aw-pr-ai-menu-collect.yml](../../.github/workflows/docs-aw-pr-ai-menu-collect.yml). +2. **`trigger-docs-aw-workflow-run.yml`** — `workflow_run` when the collect workflow completes successfully; calls [docs-aw-pr-ai-menu.yml](../../.github/workflows/docs-aw-pr-ai-menu.yml) to download the artifact and post the menu from trusted base-repo context. -| File | Triggers | Role | -|------|----------|------| -| `trigger-docs-aw.yml` | `issues` opened; `issue_comment` edited; `pull_request` (opened, reopened, synchronize, ready_for_review); `workflow_run` on this workflow when the completed run succeeded and its originating event was `pull_request`; `workflow_dispatch` (optional `issue_number`, `pull_request_number`) | Dispatches `docs-aw.yml` with relayed event JSON | -| `docs-aw.yml` | `workflow_dispatch` only | Entrypoint; calls `docs-aw-ingress.yml` | +Menu checkbox handling (`issue_comment`) uses `trigger-docs-aw-issue-comment.yml`. Manual refresh uses `workflow_dispatch` on `trigger-docs-aw-issues.yml` (issue menu) or `trigger-docs-aw-workflow-run.yml` (PR menu). Fork checkbox triggers require org membership (enforced in `scripts/docs/pr-menu/evaluate-trigger.js`). -Route-specific conditions (for example PR vs non-PR issue comments, menu checkbox transitions) are enforced inside the `docs-aw-*` reusable workflows after ingress routing. +## Configuration -## Split PR menu pattern +Top-level permissions on every client template: -The PR AI menu uses a fork-safe collect leg and a privileged post leg within one trigger: +- `contents: read` -1. **`pull_request`** on `trigger-docs-aw.yml` → ingress `route-pr-ai-menu-collect` uploads a `pr-number` artifact. -2. **`workflow_run`** when that trigger completes successfully (`workflow_run.event == pull_request`) → ingress `route-pr-ai-menu` downloads the artifact and posts the menu from trusted base-repo context. The dispatch job runs only for that privileged leg (not for `workflow_run` chains where the parent event was already `workflow_run`), preventing an infinite re-dispatch loop. +Control-plane `docs-aw-*` workflows declare permissions on **each job** (workflow root is `contents: read` only). Jobs that call `gh-aw-*.lock.yml` should match the upstream lock workflow permissions. -Menu checkbox handling (`issue_comment`) and manual refresh (`workflow_dispatch`) use the same unified trigger. +Job-level permissions on `run-aw` must be at least as permissive as the union of all route jobs in the called event orchestrator (see table below). -On `pull_request` and on the privileged `workflow_run` leg (`workflow_run.event == pull_request`), the trigger posts commit status context `Documentation Agentic Workflow Execution` on the PR head SHA with `target_url` pointing at the dispatched `docs-aw.yml` run (`runUrlHtml` from `workflow-dispatch`). This is traceability only; the trigger does not wait for ingress or routed workflows to finish. +| Client template | `run-aw` job permissions (union of callee jobs) | +|-----------------|-----------------------------------------------| +| `trigger-docs-aw-issues.yml` | `actions: read`, `contents: read`, `discussions: write`, `issues: write`, `pull-requests: write` | +| `trigger-docs-aw-issue-comment.yml` | `actions: read`, `checks: read`, `contents: read`, `discussions: write`, `issues: write`, `pull-requests: write` | +| `trigger-docs-aw-pull-request.yml` | `actions: write`, `contents: read` | +| `trigger-docs-aw-workflow-run.yml` | `actions: read`, `checks: read`, `contents: read`, `issues: write`, `pull-requests: write` | -## Permissions +Required secret mapping (templates that call agent lock workflows): -Control-plane `docs-aw-*` workflows declare permissions on **each job** (workflow root is `contents: read` only). +- `COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}` -| Client workflow | Job permissions (minimum) | -|-----------------|---------------------------| -| `trigger-docs-aw.yml` | `actions: write`, `statuses: write` (dispatch job uses `GITHUB_TOKEN`; status used for PR traceability) | -| `docs-aw.yml` | `actions: write`, `checks: read`, `contents: read`, `discussions: write`, `id-token: write`, `issues: write`, `pull-requests: write` (ingress job ceiling for all routes) | +## Migration from per-route client templates -Routed workflows (`docs-aw-ai-menu.yml`, `docs-aw-pr-ai-menu.yml`) require `issues: write`, `pull-requests: write`, and related scopes on agent jobs. +1. Merge distribution PRs that replace `trigger-docs-aw-ai-menu.yml`, `trigger-docs-aw-pr-ai-menu-collect.yml`, and `trigger-docs-aw-pr-ai-menu.yml` with the four event-scoped clients above. +2. Distribution removes client paths that are no longer in the template tree. +3. Update Backstage `workflow_ref` / token policies to reference the new client workflow files. -## Migration from per-workflow `trigger-docs-aw-*.yml` +## Migration from monolithic `docs-aw.yml` -1. Merge distribution PRs that add `trigger-docs-aw.yml` and `docs-aw.yml`. -2. Delete legacy `trigger-docs-aw-ai-menu.yml`, `trigger-docs-aw-pr-ai-menu-collect.yml`, and `trigger-docs-aw-pr-ai-menu.yml` from the consumer repository. -3. Update Backstage `workflow_ref` / token policies to reference **`trigger-docs-aw.yml`**. +1. Merge distribution PRs that add event-scoped `trigger-docs-aw-*.yml` files. +2. Delete `.github/workflows/docs-aw.yml` in the consumer repository. Remove legacy per-route client files if present; distribution drops paths that are no longer in the template tree. +3. Update Backstage `workflow_ref` / token policies to reference each installed **`trigger-docs-aw-*.yml`** client workflow file. ## References -- [docs-aw-ingress.md](docs-aw-ingress.md) - [docs-aw-ai-menu.md](docs-aw-ai-menu.md) - [docs-aw-pr-ai-menu.md](docs-aw-pr-ai-menu.md) +- [docs/operations/distribute-client-workflow.md](../operations/distribute-client-workflow.md) - [aw-prelude.md](aw-prelude.md) +- [oblt-aw-client-template.md](oblt-aw-client-template.md) diff --git a/docs/workflows/docs-aw-ingress.md b/docs/workflows/docs-aw-ingress.md deleted file mode 100644 index eeab381f..00000000 --- a/docs/workflows/docs-aw-ingress.md +++ /dev/null @@ -1,33 +0,0 @@ -# Workflow: `docs-aw-ingress.yml` - -## Overview - -Central ingress reusable for Documentation agentic workflows. Called from consumer `docs-aw.yml` via `workflow_call` with relayed event context from `trigger-docs-aw.yml`. - -1. **`aw-prelude`** — reads dashboard `enabled-workflows` / `effective-raw` and token policy once per ingress run -2. **Route jobs** — Each `route-*` job gates on event eligibility and dashboard enablement; only matching routes call the corresponding `docs-aw-*` reusable with relayed event context - -Route ids and workflow files are declared in [`config/docs/workflow-registry.json`](../../config/docs/workflow-registry.json) `ingress_routes`; CI validates that registry entries match `route-*` jobs in this workflow and that each `route-*` job declares `permissions` covering the called `docs-aw-*` workflow. - -## Inputs - -| Input | Required | Description | -|-------|----------|-------------| -| `trigger-source` | yes | Client trigger workflow basename | -| `ingress-event-name` | yes | Original `github.event_name` | -| `ingress-event-action` | no | Original `github.event.action` | -| `ingress-event-payload-json` | yes | `toJSON(github.event)` from the trigger | -| `caller-ref` | yes | Original `github.ref` | -| `caller-sha` | yes | Original `github.sha` | -| `caller-run-id` | yes | Original `github.run_id` | - -## Secrets - -| Secret | Required | Used by | -|--------|----------|---------| -| `COPILOT_GITHUB_TOKEN` | no | Routed workflows that call GH-AW locks | - -## References - -- [docs/workflows/docs-aw-client-template.md](docs-aw-client-template.md) -- [docs/routing/README.md](../routing/README.md) diff --git a/docs/workflows/docs-aw-pr-ai-menu.md b/docs/workflows/docs-aw-pr-ai-menu.md index e7ee35cf..72710dd2 100644 --- a/docs/workflows/docs-aw-pr-ai-menu.md +++ b/docs/workflows/docs-aw-pr-ai-menu.md @@ -4,18 +4,18 @@ Source file: [.github/workflows/docs-aw-pr-ai-menu.yml](../../.github/workflows/docs-aw-pr-ai-menu.yml) -Reusable implementation for the Docs PR AI menu. Routed from `docs-aw-ingress.yml` when the client `trigger-docs-aw.yml` relays a matching event. +Reusable implementation for the Docs PR AI menu. Event-scoped client templates call `docs-aw-event-issue-comment.yml` or `docs-aw-event-workflow-run.yml`, which fan out to this route on supported events. ## Prerequisites -- Triggered via `workflow_call` from [docs-aw-ingress.yml](../../.github/workflows/docs-aw-ingress.yml) (`route-pr-ai-menu`). +- Triggered via `workflow_call` from [docs-aw-event-workflow-run.yml](../../.github/workflows/docs-aw-event-workflow-run.yml) or [docs-aw-event-issue-comment.yml](../../.github/workflows/docs-aw-event-issue-comment.yml) after client templates in [docs-aw-client-template.md](docs-aw-client-template.md). - Optional secret input: `COPILOT_GITHUB_TOKEN` (`required: false` at this workflow boundary). ## Usage Jobs and routing behavior: -1. `post-menu` posts or refreshes the PR AI menu when the routed event is a successful `workflow_run` (after a fork-safe `pull_request` collect leg on the same `trigger-docs-aw.yml`) or `workflow_dispatch`. The privileged leg downloads the PR number artifact and never uses `pull_request_target`. +1. `post-menu` posts or refreshes the PR AI menu when the routed event is a successful `workflow_run` of [trigger-docs-aw-pull-request.yml](../../.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pull-request.yml) or `workflow_dispatch`. The collect leg uses fork-safe `pull_request`; the privileged leg downloads the PR number artifact and never uses `pull_request_target`. 2. `evaluate-trigger` runs on routed `issue_comment` events for PRs when an existing AI menu bot comment was edited (`` and `` markers). On fork PRs, only organization members may trigger the docs review path. 3. `run-docs-review` calls `elastic/docs-actions/.github/workflows/gh-aw-docs-review.lock.yml@v1` when `docs_review_triggered == 'true'`, with `review-scope: repo-wide-markdown`. 4. Refresh jobs update the AI PR menu comment after trigger evaluation and after the downstream review run. diff --git a/docs/workflows/oblt-aw-client-template.md b/docs/workflows/oblt-aw-client-template.md index 492e3f06..1d036ab8 100644 --- a/docs/workflows/oblt-aw-client-template.md +++ b/docs/workflows/oblt-aw-client-template.md @@ -1,17 +1,18 @@ -# Workflow: Client templates `trigger-oblt-aw.yml` and `oblt-aw.yml` +# Workflow: Client templates `trigger-oblt-aw-*.yml` ## Overview **Source of truth (edit here only):** [.github/remote-workflow-template/obs/.github/workflows/](../../.github/remote-workflow-template/obs/.github/workflows/) -Consumer repositories install **two** client workflows: +## Event-scoped client model -| File | Role | -|------|------| -| `trigger-oblt-aw.yml` | Declares all supported GitHub events; one job dispatches the entrypoint via [`benc-uk/workflow-dispatch`](https://github.com/benc-uk/workflow-dispatch) and posts a PR commit status linking to the dispatched run | -| `oblt-aw.yml` | `workflow_dispatch` receiver; calls `elastic/oblt-aw/.github/workflows/oblt-aw-ingress.yml@main` with relayed event context | +Client templates are grouped by **GitHub event family** so co-triggered routes share one dashboard read and one allow-list load per workflow run. Each event-scoped client calls an orchestrator reusable (`oblt-aw-event-*.yml`) that runs [aw-prelude.yml](aw-prelude.md) once, then fans out to per-route `oblt-aw-*` workflows. -Split per-workflow `trigger-oblt-aw-*.yml` files are **not** distributed anymore. +```yaml +uses: elastic/oblt-aw/.github/workflows/oblt-aw-event-pull-request.yml@main +``` + +Per-route dashboard gating uses the required `shared-proceed` input (and related shared allow-list fields) passed from [aw-prelude.yml](aw-prelude.md) via each `oblt-aw-event-*` orchestrator. ### Architecture @@ -19,85 +20,87 @@ Split per-workflow `trigger-oblt-aw-*.yml` files are **not** distributed anymore flowchart TB subgraph Consumer["Consumer .github/workflows/"] EVT["GitHub event"] - TRG["trigger-oblt-aw.yml\nall supported on:"] - AW["oblt-aw.yml\non: workflow_dispatch"] - EVT --> TRG - TRG -->|"workflow-dispatch action + event payload"| AW + C_PR["trigger-oblt-aw-pull-request.yml\non: pull_request"] + C_ISS["trigger-oblt-aw-issues.yml\non: issues, workflow_dispatch"] + C_COM["trigger-oblt-aw-issue-comment.yml\non: issue_comment"] + C_SCH["trigger-oblt-aw-schedule.yml\ntrigger-oblt-aw-status.yml"] + DASH["Issue: [oblt-aw] Control Plane Dashboard"] + EVT --> C_PR + EVT --> C_ISS + EVT --> C_COM + EVT -.->|on: no match| C_SCH end subgraph OBLT["elastic/oblt-aw"] - ING["oblt-aw-ingress.yml\nroute-* if gates → matching oblt-aw-*"] - AW -->|workflow_call| ING - ING --> R1["oblt-aw-automerge.yml"] - ING --> R2["oblt-aw-dependency-review.yml"] - ING --> R3["other planned routes"] + ORCH["oblt-aw-event-* orchestrator"] + CTX["aw-prelude"] + R["oblt-aw-* route"] + AG["Agent steps"] + LOCK["Upstream gh-aw lock"] + ORCH --> CTX + ORCH --> R + R --> AG --> LOCK end -``` -### Trigger events (`trigger-oblt-aw.yml`) + DASH -.->|checkboxes| CTX + C_PR --> ORCH + C_ISS --> ORCH + C_COM --> ORCH + C_SCH --> ORCH +``` -| Trigger | Types / notes | -|---------|----------------| -| `schedule` | `0 6 * * *` | -| `workflow_dispatch` | Manual replay | -| `issues` | `opened`, `labeled` | -| `issue_comment` | `created` | -| `pull_request` | `opened`, `synchronize`, `reopened`, `labeled` | -| `status` | Buildkite failure routing handled in ingress | +Full platform view (distribution, dashboard sync, before/after ingress): [architecture overview — split-trigger diagrams](../architecture/overview.md#split-trigger-vs-monolithic-ingress). -### Context relayed to `oblt-aw.yml` +### Template index -| Input | Source | -|-------|--------| -| `trigger-source` | `github.workflow` | -| `event-name` | `github.event_name` | -| `event-action` | `github.event.action` | -| `event-payload-json` | `toJSON(github.event)` | -| `caller-ref` | `github.ref` | -| `caller-sha` | `github.sha` | -| `caller-run-id` | `github.run_id` | +| Client template | Triggers | Reusable workflow | +|-----------------|----------|-------------------| +| `trigger-oblt-aw-pull-request.yml` | `pull_request` (opened, synchronize, reopened, labeled) | `oblt-aw-event-pull-request.yml` → automerge, dependency-review | +| `trigger-oblt-aw-issues.yml` | `issues` (opened, labeled), `workflow_dispatch` | `oblt-aw-event-issues.yml` → issue-triage, duplicate-issue-detector, security triage/fixer, resource triage/fixer | +| `trigger-oblt-aw-issue-comment.yml` | `issue_comment` created | `oblt-aw-event-issue-comment.yml` → issue-fixer, mention-in-issue | +| `trigger-oblt-aw-schedule.yml` | `schedule` (daily 06:00 UTC), `workflow_dispatch` | `oblt-aw-event-schedule.yml` → agent-suggestions, autodoc, security-detector, resource-not-accessible detector | +| `trigger-oblt-aw-status.yml` | `status` (Buildkite failure only, job `if`) | `oblt-aw-event-status.yml` → estc-pr-buildkite-detective | -### Permissions +Route-specific conditions (labels, `/ai` comment prefix, allow-listed PR authors, and so on) are enforced inside each `oblt-aw-*` reusable workflow after prelude gating. -**`trigger-oblt-aw.yml`** +## Configuration -| Scope | Job | Why | -|-------|-----|-----| -| `contents: read` | workflow root | Default | -| `actions: write` | `dispatch-entrypoint` | `GITHUB_TOKEN` — REST `workflow_dispatch` for `oblt-aw.yml` in the same repository | -| `statuses: write` | `dispatch-entrypoint` | `GITHUB_TOKEN` — PR commit status with link to dispatched `oblt-aw.yml` run | +Top-level permissions on every client template: -The trigger uses `secrets.GITHUB_TOKEN` only (no `create-token`); same-repo dispatch and commit statuses do not need Backstage OIDC. +- `contents: read` -The dispatch step does **not** wait for `oblt-aw.yml` to finish. On `pull_request` events, a follow-up step posts commit status context `Observability Agentic Workflow Execution` on the PR head SHA with `target_url` set to the `runUrlHtml` output from `workflow-dispatch` (traceability only; `state: success` means dispatch succeeded, not that ingress or routed workflows completed). Do not add this context as a required check unless you intend to gate merges on dispatch alone. +Control-plane `oblt-aw-*` workflows declare permissions on **each job** (workflow root is `contents: read` only). Jobs that call `gh-aw-*.lock.yml` should match the upstream lock workflow permissions. -**`oblt-aw.yml`** +Job-level permissions on `run-aw` must be at least as permissive as the union of all route jobs in the called event orchestrator (see table below). -| Scope | Job | Why | -|-------|-----|-----| -| `contents: read` | workflow root | Default | -| `actions: write` | `ingress` | Ingress may dispatch nested workflows | -| `contents: write` | `ingress` | Routed workflows that open or update PRs | -| `discussions: write` | `ingress` | Routed GH-AW workflows that post discussions | -| `id-token: write` | `ingress` | Ephemeral tokens in routed workflows | -| `issues: write` | `ingress` | Routed workflows that create or update issues | -| `pull-requests: write` | `ingress` | Routed workflows that create or update PRs | +| Client template | `run-aw` job permissions (union of callee jobs) | +|-----------------|-----------------------------------------------| +| `trigger-oblt-aw-pull-request.yml` | `actions: read`, `contents: write`, `discussions: write`, `id-token: write`, `issues: write`, `pull-requests: write` | +| `trigger-oblt-aw-issues.yml` | `actions: read`, `contents: write`, `discussions: write`, `id-token: write`, `issues: write`, `pull-requests: write` | +| `trigger-oblt-aw-issue-comment.yml` | `actions: read`, `contents: write`, `discussions: write`, `issues: write`, `pull-requests: write` | +| `trigger-oblt-aw-schedule.yml` | `actions: read`, `contents: write`, `id-token: write`, `issues: write`, `pull-requests: write` | +| `trigger-oblt-aw-status.yml` | `actions: read`, `contents: read`, `issues: read`, `pull-requests: write` | ### Secrets | Secret | Templates | |--------|-----------| -| `COPILOT_GITHUB_TOKEN` | `oblt-aw.yml` (forwarded to ingress and routed workflows) | -| `BUILDKITE_LOGS_API_TOKEN` | `oblt-aw.yml` → ingress as `BUILDKITE_API_TOKEN` (Buildkite detective route) | +| `COPILOT_GITHUB_TOKEN` | All event-scoped templates except resource-not-accessible fixer routes inside orchestrators (those use `secrets: inherit` on the route job) | +| `BUILDKITE_LOGS_API_TOKEN` → `BUILDKITE_API_TOKEN` | `trigger-oblt-aw-status.yml` only | + +## Migration from per-route client templates + +1. Merge distribution PRs that replace per-route `trigger-oblt-aw-*.yml` files with the three event-scoped clients above (plus unchanged schedule/status templates). +2. Distribution removes client paths that are no longer in the template tree. +3. Update Backstage `workflow_ref` / token policies to reference the new client workflow files (for example `trigger-oblt-aw-pull-request.yml` instead of separate automerge and dependency-review triggers). -## Migration from split triggers +## Migration from monolithic entrypoint -1. Merge distribution PRs that install `trigger-oblt-aw.yml` and `oblt-aw.yml`. -2. Remove legacy `trigger-oblt-aw-*.yml` per-workflow client files (distribution `remove_files` handles drops). -3. Register Backstage `workflow_ref` for **`oblt-aw.yml`** (and routed control-plane workflows) when your vault model requires `create-token`; **`trigger-oblt-aw.yml` does not call `create-token`**. +1. Merge distribution PRs that add event-scoped `trigger-oblt-aw-*.yml` files. +2. Delete `.github/workflows/oblt-aw.yml` and stop calling `oblt-aw-ingress` in the consumer repository. Remove any legacy per-workflow client files named `oblt-aw-*.yml` or `trg-oblt-aw-*.yml`; distribution drops paths that are no longer in the template tree. +3. Update Backstage `workflow_ref` / token policies to reference each installed **`trigger-oblt-aw-*.yml`** client workflow file. ## References - [docs/operations/distribute-client-workflow.md](../operations/distribute-client-workflow.md) -- [oblt-aw-ingress.md](oblt-aw-ingress.md) - [aw-prelude.md](aw-prelude.md) diff --git a/docs/workflows/oblt-aw-ingress.md b/docs/workflows/oblt-aw-ingress.md deleted file mode 100644 index e6773cb5..00000000 --- a/docs/workflows/oblt-aw-ingress.md +++ /dev/null @@ -1,34 +0,0 @@ -# Workflow: `oblt-aw-ingress.yml` - -## Overview - -Central ingress reusable for Observability agentic workflows. Called from consumer `oblt-aw.yml` via `workflow_call` with relayed event context from `trigger-oblt-aw.yml`. - -1. **`aw-prelude`** — reads dashboard `enabled-workflows` / `effective-raw`, PR/issue allow lists, and token policy once per ingress run -2. **Route jobs** — Each `route-*` job carries an `if:` gate for event eligibility and dashboard enablement; only matching routes call the corresponding `oblt-aw-*` reusable with relayed event context and prelude allow lists. Individual `oblt-aw-*` workflows do **not** call `aw-prelude`; route eligibility is decided entirely in ingress. - -Route ids and workflow files are declared in [`config/obs/workflow-registry.json`](../../config/obs/workflow-registry.json) `ingress_routes`; CI validates that registry entries match `route-*` jobs in this workflow and that each `route-*` job declares `permissions` covering the called `oblt-aw-*` workflow (reusable-workflow token ceiling). - -## Inputs - -| Input | Required | Description | -|-------|----------|-------------| -| `trigger-source` | yes | Client trigger workflow basename | -| `ingress-event-name` | yes | Original `github.event_name` | -| `ingress-event-action` | no | Original `github.event.action` | -| `ingress-event-payload-json` | yes | `toJSON(github.event)` from the trigger | -| `caller-ref` | yes | Original `github.ref` | -| `caller-sha` | yes | Original `github.sha` | -| `caller-run-id` | yes | Original `github.run_id` | - -## Secrets - -| Secret | Required | Used by | -|--------|----------|---------| -| `COPILOT_GITHUB_TOKEN` | no | Routed workflows that call GH-AW locks | -| `BUILDKITE_API_TOKEN` | no | `oblt-aw-estc-pr-buildkite-detective.yml` route | - -## References - -- [docs/workflows/oblt-aw-client-template.md](oblt-aw-client-template.md) -- [docs/routing/README.md](../routing/README.md) diff --git a/docs/workflows/sync-control-plane-dashboard.md b/docs/workflows/sync-control-plane-dashboard.md index a144d99e..c5da3ba1 100644 --- a/docs/workflows/sync-control-plane-dashboard.md +++ b/docs/workflows/sync-control-plane-dashboard.md @@ -8,7 +8,7 @@ This workflow creates or updates the **single** Control Plane Dashboard issue in ## Prerequisites -- Per-org `config//workflow-registry.json` — workflow metadata (`id`, `name`, `description`, `maturity`, `default_enabled`, `ingress_routes`, optional `section_title`) +- Per-org `config//workflow-registry.json` — workflow metadata (`id`, `name`, `description`, `maturity`, `default_enabled`, `control_plane_workflows`, optional `section_title`) - Per-org `config//active-repositories.json` — target repositories for that org’s workflows - Token policy configured for [elastic/oblt-actions/github/create-token@v1](https://github.com/elastic/oblt-actions/blob/v1/github/create-token/action.yml) - [GitHub CLI (`gh`)](https://cli.github.com/) available in `PATH` (required by `scripts/sync_control_plane_dashboard.py` for `gh api` and issue pinning) diff --git a/scripts/build_repos_matrix.py b/scripts/build_repos_matrix.py index 82ba6198..5c218726 100644 --- a/scripts/build_repos_matrix.py +++ b/scripts/build_repos_matrix.py @@ -20,7 +20,8 @@ Unions ``config//active-repositories.json`` for each discovered org tree, then writes to GITHUB_OUTPUT: -- repos: JSON array of {"repository": "owner/repo"} for matrix strategy +- repos: JSON array of {"repository": "owner/repo", "token-policy": "..."} for matrix strategy + (``token-policy`` is empty when not configured in active-repositories.json) - has_repos: "true" or "false" - repos_count: number of repositories @@ -33,7 +34,11 @@ import sys from pathlib import Path -from common import merge_active_repositories_from_org_trees, write_outputs +from common import ( + merge_active_repositories_from_org_trees, + merge_repository_token_policies_from_org_trees, + write_outputs, +) def main() -> int: @@ -41,7 +46,14 @@ def main() -> int: root = Path(__file__).resolve().parent.parent config_dir = root / "config" repos = merge_active_repositories_from_org_trees(config_dir) - matrix = [{"repository": repo} for repo in repos] + token_policies = merge_repository_token_policies_from_org_trees(config_dir) + matrix = [ + { + "repository": repo, + "token-policy": token_policies.get(repo, ""), + } + for repo in repos + ] write_outputs( { diff --git a/scripts/build_target_operations.py b/scripts/build_target_operations.py index 7320355b..9b49e96a 100644 --- a/scripts/build_target_operations.py +++ b/scripts/build_target_operations.py @@ -19,7 +19,12 @@ import subprocess import sys -from common import discover_repo_org_assignments, parse_repositories, write_outputs +from common import ( + discover_repo_org_assignments, + merge_repository_token_policies_from_org_trees, + parse_repositories, + write_outputs, +) REMOTE_TEMPLATE_DIR = pathlib.Path(".github/remote-workflow-template") ZERO_SHA = "0000000000000000000000000000000000000000" @@ -178,6 +183,7 @@ def main() -> int: config_dir = pathlib.Path("config") current_assignments = discover_repo_org_assignments(config_dir) + token_policies = merge_repository_token_policies_from_org_trees(config_dir) previous_assignments = read_previous_repo_org_assignments(base_ref) @@ -220,6 +226,7 @@ def dst_paths(files: list[dict[str, str]]) -> set[str]: "operation": "install", "files": files, "remove_files": remove_files, + "token-policy": token_policies.get(repo, ""), } ) @@ -231,6 +238,7 @@ def dst_paths(files: list[dict[str, str]]) -> set[str]: "repository": repo, "operation": "remove", "files": files, + "token-policy": token_policies.get(repo, ""), } ) diff --git a/scripts/common.py b/scripts/common.py index b4c09ac8..4c57e6ff 100644 --- a/scripts/common.py +++ b/scripts/common.py @@ -71,6 +71,7 @@ class ActiveRepositoryEntry: """One repository row from active-repositories.json.""" repository: str + token_policy: str def _parse_repository_entry(item: object) -> ActiveRepositoryEntry: @@ -80,7 +81,7 @@ def _parse_repository_entry(item: object) -> ActiveRepositoryEntry: raise SystemExit( f"Invalid repository entry: {item!r}. Expected 'owner/repo'" ) - return ActiveRepositoryEntry(repository=repo) + return ActiveRepositoryEntry(repository=repo, token_policy="") if isinstance(item, dict): raw_repo = item.get("repository") if not isinstance(raw_repo, str) or "/" not in raw_repo.strip(): @@ -88,7 +89,18 @@ def _parse_repository_entry(item: object) -> ActiveRepositoryEntry: f"Invalid repository entry: {item!r}. " "Object entries require string 'repository' in 'owner/repo' form" ) - return ActiveRepositoryEntry(repository=raw_repo.strip()) + repo = raw_repo.strip() + policy = item.get("token-policy", "") + if policy is None: + policy = "" + if not isinstance(policy, str): + raise SystemExit( + f"Invalid token-policy for {repo!r}: expected string, got {type(policy).__name__}" + ) + return ActiveRepositoryEntry( + repository=repo, + token_policy=policy.strip(), + ) raise SystemExit( f"Invalid repository entry: {item!r}. " "Expected 'owner/repo' string or object with 'repository'" @@ -101,8 +113,9 @@ def parse_active_repository_entries(content: str) -> list[ActiveRepositoryEntry] Supports: - - Object: ``{"repositories": ["owner/repo", {"repository": "owner/repo"}, ...]}`` - - List: same entry shapes at the top level (legacy migration) + - Object: ``{"repositories": [{"repository": "owner/repo", "token-policy": ""}, ...]}`` + - List: same object entry shapes at the top level (legacy migration) + - String entries in ``repositories`` (legacy migration only; prefer objects in config files) """ data = json.loads(content) if content else {"repositories": []} if isinstance(data, dict): @@ -118,7 +131,14 @@ def parse_active_repository_entries(content: str) -> list[ActiveRepositoryEntry] entries = [_parse_repository_entry(item) for item in repositories] by_repo: dict[str, ActiveRepositoryEntry] = {} for entry in entries: - if entry.repository in by_repo: + previous = by_repo.get(entry.repository) + if previous is not None: + if previous.token_policy != entry.token_policy: + raise SystemExit( + f"Duplicate repository {entry.repository!r} with conflicting " + f"token-policy values: {previous.token_policy!r} vs " + f"{entry.token_policy!r}" + ) continue by_repo[entry.repository] = entry return sorted(by_repo.values(), key=lambda e: e.repository) @@ -129,6 +149,37 @@ def parse_repositories(content: str) -> list[str]: return [entry.repository for entry in parse_active_repository_entries(content)] +def merge_repository_token_policies_from_org_trees(config_dir: Path) -> dict[str, str]: + """ + Map repository to a non-empty token-policy from org ``active-repositories.json`` files. + + When the same repository appears in multiple org trees with different non-empty + policies, exits with an error. + """ + policies: dict[str, str] = {} + for org_dir in discover_org_config_dirs(config_dir): + path = org_dir / "active-repositories.json" + for entry in parse_active_repository_entries(path.read_text(encoding="utf-8")): + if not entry.token_policy: + continue + previous = policies.get(entry.repository) + if previous is not None and previous != entry.token_policy: + raise SystemExit( + f"Conflicting token-policy for {entry.repository!r} across org " + f"config: {previous!r} vs {entry.token_policy!r} " + f"(org {org_dir.name!r})" + ) + policies[entry.repository] = entry.token_policy + return policies + + +def lookup_repository_token_policy(config_dir: Path, repository: str) -> str: + """Return configured token-policy for ``repository``, or ``""`` when unset.""" + return merge_repository_token_policies_from_org_trees(config_dir).get( + repository.strip(), "" + ) + + def discover_org_config_dirs(config_dir: Path) -> list[Path]: """ Return sorted org root directories under ``config_dir`` (direct children only). diff --git a/scripts/docs/issue-menu/evaluate-trigger.js b/scripts/docs/issue-menu/evaluate-trigger.js index c152fd26..d376e91e 100644 --- a/scripts/docs/issue-menu/evaluate-trigger.js +++ b/scripts/docs/issue-menu/evaluate-trigger.js @@ -15,13 +15,11 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { parseMenuState } = require('./lib.js'); module.exports = async ({ context, core }) => { - const payload = relayedPayload(context); - const body = payload.comment?.body || ''; - const previousBody = payload.changes?.body?.from || ''; + const body = context.payload.comment.body || ''; + const previousBody = context.payload.changes?.body?.from || ''; const previousState = parseMenuState(previousBody); const currentState = parseMenuState(body); diff --git a/scripts/docs/issue-menu/post-menu.js b/scripts/docs/issue-menu/post-menu.js index e45a6a09..897a729f 100644 --- a/scripts/docs/issue-menu/post-menu.js +++ b/scripts/docs/issue-menu/post-menu.js @@ -15,16 +15,14 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const payload = relayedPayload(context); - const workflowDispatchIssueNumber = payload.inputs?.issue_number; + const workflowDispatchIssueNumber = context.payload.inputs?.issue_number; const issueNumber = context.eventName === 'workflow_dispatch' ? Number(workflowDispatchIssueNumber) - : payload.issue?.number; + : context.payload.issue.number; if (!issueNumber) { core.setFailed('Issue number is required for workflow_dispatch runs.'); diff --git a/scripts/docs/issue-menu/refresh-after-docs-issue-scope.js b/scripts/docs/issue-menu/refresh-after-docs-issue-scope.js index 1242be52..7de67b22 100644 --- a/scripts/docs/issue-menu/refresh-after-docs-issue-scope.js +++ b/scripts/docs/issue-menu/refresh-after-docs-issue-scope.js @@ -15,11 +15,10 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const issueNumber = relayedPayload(context).issue?.number; + const issueNumber = context.payload.issue.number; const progressUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const issueScopeResult = process.env.ISSUE_SCOPE_RESULT || ''; diff --git a/scripts/docs/issue-menu/refresh-after-docs-triage.js b/scripts/docs/issue-menu/refresh-after-docs-triage.js index 19ab54ab..27cb6882 100644 --- a/scripts/docs/issue-menu/refresh-after-docs-triage.js +++ b/scripts/docs/issue-menu/refresh-after-docs-triage.js @@ -15,11 +15,10 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const issueNumber = relayedPayload(context).issue?.number; + const issueNumber = context.payload.issue.number; const progressUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const triageResult = process.env.TRIAGE_RESULT || ''; diff --git a/scripts/docs/issue-menu/refresh-after-trigger.js b/scripts/docs/issue-menu/refresh-after-trigger.js index 2fc55cfb..f8c46a82 100644 --- a/scripts/docs/issue-menu/refresh-after-trigger.js +++ b/scripts/docs/issue-menu/refresh-after-trigger.js @@ -15,11 +15,10 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const issueNumber = relayedPayload(context).issue?.number; + const issueNumber = context.payload.issue.number; const progressUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const statusOverrides = {}; diff --git a/scripts/docs/lib/relayed-event.js b/scripts/docs/lib/relayed-event.js deleted file mode 100644 index 1cb2c137..00000000 --- a/scripts/docs/lib/relayed-event.js +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2026-2027 Elasticsearch B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -'use strict'; - -/** - * Original webhook payload for workflow_call routes that relay - * ingress-event-payload-json. github-script context.payload is the - * workflow_call envelope, not the consumer pull_request/issue event. - */ -function relayedPayload(context) { - const raw = process.env.INGRESS_EVENT_JSON; - if (typeof raw === 'string' && raw.trim() !== '') { - return JSON.parse(raw); - } - return context.payload; -} - -module.exports = { relayedPayload }; diff --git a/scripts/docs/pr-menu/evaluate-trigger.js b/scripts/docs/pr-menu/evaluate-trigger.js index e619dfca..2f423c8c 100644 --- a/scripts/docs/pr-menu/evaluate-trigger.js +++ b/scripts/docs/pr-menu/evaluate-trigger.js @@ -15,20 +15,13 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { parseMenuState } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const payload = relayedPayload(context); - const pullNumber = payload.issue?.number; - if (!pullNumber) { - core.setFailed('Pull request number is required.'); - return; - } const { data: pullRequest } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: pullNumber, + pull_number: context.payload.issue.number, }); const baseRepoFullName = `${context.repo.owner}/${context.repo.repo}`; const isForkPR = pullRequest.head.repo.full_name !== baseRepoFullName; @@ -50,8 +43,8 @@ module.exports = async ({ github, context, core }) => { } } - const body = payload.comment?.body || ''; - const previousBody = payload.changes?.body?.from || ''; + const body = context.payload.comment.body || ''; + const previousBody = context.payload.changes?.body?.from || ''; const previousState = parseMenuState(previousBody); const currentState = parseMenuState(body); diff --git a/scripts/docs/pr-menu/post-menu.js b/scripts/docs/pr-menu/post-menu.js index 5e063b81..5218bb2b 100644 --- a/scripts/docs/pr-menu/post-menu.js +++ b/scripts/docs/pr-menu/post-menu.js @@ -15,17 +15,15 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const payload = relayedPayload(context); - const workflowDispatchPrNumber = payload.inputs?.pull_request_number; + const workflowDispatchPrNumber = context.payload.inputs?.pull_request_number; const envPrNumber = process.env.PULL_REQUEST_NUMBER; const pullRequestNumber = Number( context.eventName === 'workflow_dispatch' ? workflowDispatchPrNumber - : envPrNumber || payload.pull_request?.number || payload.issue?.number, + : envPrNumber || context.payload.pull_request?.number, ); if (!pullRequestNumber || Number.isNaN(pullRequestNumber)) { diff --git a/scripts/docs/pr-menu/refresh-after-docs-review.js b/scripts/docs/pr-menu/refresh-after-docs-review.js index 47b15177..d3c82adc 100644 --- a/scripts/docs/pr-menu/refresh-after-docs-review.js +++ b/scripts/docs/pr-menu/refresh-after-docs-review.js @@ -15,11 +15,10 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const pullRequestNumber = relayedPayload(context).issue?.number; + const pullRequestNumber = context.payload.issue.number; const progressUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const docsReviewResult = process.env.DOCS_REVIEW_RESULT || ''; diff --git a/scripts/docs/pr-menu/refresh-after-trigger.js b/scripts/docs/pr-menu/refresh-after-trigger.js index 2a75bf0e..ec372f11 100644 --- a/scripts/docs/pr-menu/refresh-after-trigger.js +++ b/scripts/docs/pr-menu/refresh-after-trigger.js @@ -15,11 +15,10 @@ 'use strict'; -const { relayedPayload } = require('../lib/relayed-event.js'); const { upsertMenuComment } = require('./lib.js'); module.exports = async ({ github, context, core }) => { - const pullRequestNumber = relayedPayload(context).issue?.number; + const pullRequestNumber = context.payload.issue.number; const progressUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; await upsertMenuComment({ diff --git a/scripts/evaluate_workflow_gates.py b/scripts/evaluate_workflow_gates.py new file mode 100644 index 00000000..7dccfd50 --- /dev/null +++ b/scripts/evaluate_workflow_gates.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# Copyright 2026-2027 Elasticsearch B.V. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Evaluate dashboard gates for multiple control-plane workflows at once. + +Used by aw-prelude.yml so event-scoped orchestrators run dashboard and +allow-list loading once, then fan out per-route proceed flags. +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +from pathlib import Path + +from common import write_outputs +from workflow_registry import build_control_plane_workflow_index + + +def _proceed_for_compound_id( + effective_raw: str, enabled_workflows_json: str, compound_id: str +) -> bool: + if not effective_raw: + return True + enabled = json.loads(enabled_workflows_json) + if not isinstance(enabled, list): + raise ValueError("enabled-workflows must be a JSON array") + return compound_id in enabled + + +def evaluate_gates( + config_dir: Path, + control_plane_workflows: list[str], + effective_raw: str, + enabled_workflows_json: str, +) -> dict[str, str]: + index = build_control_plane_workflow_index(config_dir) + proceed_by_workflow: dict[str, str] = {} + for basename in control_plane_workflows: + if basename not in index: + known = ", ".join(sorted(index)) + raise ValueError( + f"control-plane workflow {basename!r} is not listed in any " + f"workflow-registry.json control_plane_workflows (known: {known})" + ) + compound_id = index[basename].compound_id + allowed = _proceed_for_compound_id( + effective_raw, enabled_workflows_json, compound_id + ) + proceed_by_workflow[basename] = "true" if allowed else "false" + return proceed_by_workflow + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Evaluate dashboard gates for multiple control-plane workflows" + ) + parser.add_argument( + "--config-dir", + type=Path, + default=Path("config"), + help="Config root containing per-org workflow-registry.json trees", + ) + parser.add_argument( + "--control-plane-workflows", + required=True, + help="JSON array of workflow basenames under .github/workflows/", + ) + parser.add_argument( + "--effective-raw", + default="", + help="Raw dashboard read ('' means all workflows enabled)", + ) + parser.add_argument( + "--enabled-workflows", + default="[]", + help="Normalized JSON array of compound org:workflow-id strings", + ) + args = parser.parse_args() + + try: + workflows = json.loads(args.control_plane_workflows) + except json.JSONDecodeError as exc: + print(f"Invalid --control-plane-workflows JSON: {exc}", file=sys.stderr) + return 1 + if not isinstance(workflows, list) or not workflows: + print( + "--control-plane-workflows must be a non-empty JSON array", + file=sys.stderr, + ) + return 1 + if not all(isinstance(name, str) and name for name in workflows): + print( + "--control-plane-workflows must contain non-empty strings", + file=sys.stderr, + ) + return 1 + + try: + proceed_by_workflow = evaluate_gates( + args.config_dir, + workflows, + args.effective_raw, + args.enabled_workflows, + ) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return 1 + + payload = json.dumps(proceed_by_workflow, separators=(",", ":")) + if os.getenv("GITHUB_OUTPUT"): + write_outputs({"proceed-by-workflow": payload}) + else: + print(payload) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/ingress_github_context.py b/scripts/ingress_github_context.py deleted file mode 100644 index 8ece80f0..00000000 --- a/scripts/ingress_github_context.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026-2027 Elasticsearch B.V. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -"""Build gh-aw prompt and setup overrides from relayed ingress event JSON.""" - -from __future__ import annotations - -import json -import shlex -from dataclasses import dataclass -from typing import Any - - -@dataclass(frozen=True) -class RelayedGitHubContext: - event_name: str | None - event_action: str | None - pull_request_number: int | None - pull_request_title: str | None - pull_request_head_ref: str | None - pull_request_head_sha: str | None - issue_number: int | None - comment_id: int | None - discussion_number: int | None - - @property - def has_actionable_target(self) -> bool: - return any( - value is not None - for value in ( - self.pull_request_number, - self.issue_number, - self.comment_id, - self.discussion_number, - ) - ) - - -def _positive_int(value: Any) -> int | None: - if isinstance(value, bool): - return None - if isinstance(value, int) and value > 0: - return value - if isinstance(value, str) and value.strip().isdigit(): - parsed = int(value.strip()) - return parsed if parsed > 0 else None - return None - - -def _optional_str(value: Any) -> str | None: - if isinstance(value, str): - stripped = value.strip() - return stripped or None - return None - - -def parse_relayed_ingress_payload(raw: str) -> dict[str, Any] | None: - raw = raw.strip() - if not raw: - return None - try: - payload: Any = json.loads(raw) - except json.JSONDecodeError: - return None - if isinstance(payload, str): - try: - payload = json.loads(payload) - except json.JSONDecodeError: - return None - if not isinstance(payload, dict): - return None - return payload - - -def extract_relayed_github_context( - payload: dict[str, Any], - *, - ingress_event_name: str | None = None, - ingress_event_action: str | None = None, -) -> RelayedGitHubContext: - pull_request = payload.get("pull_request") - issue = payload.get("issue") - comment = payload.get("comment") - discussion = payload.get("discussion") - inputs = payload.get("inputs") - - pull_request_number: int | None = None - pull_request_title: str | None = None - pull_request_head_ref: str | None = None - pull_request_head_sha: str | None = None - - if isinstance(pull_request, dict): - pull_request_number = _positive_int(pull_request.get("number")) - pull_request_title = _optional_str(pull_request.get("title")) - head = pull_request.get("head") - if isinstance(head, dict): - pull_request_head_ref = _optional_str(head.get("ref")) - pull_request_head_sha = _optional_str(head.get("sha")) - - issue_number = ( - _positive_int(issue.get("number")) if isinstance(issue, dict) else None - ) - if ( - pull_request_number is None - and isinstance(issue, dict) - and issue.get("pull_request") is not None - ): - pull_request_number = issue_number - pull_request_title = _optional_str(issue.get("title")) - - if isinstance(inputs, dict): - if pull_request_number is None: - pull_request_number = _positive_int(inputs.get("pull_request_number")) - if issue_number is None: - issue_number = _positive_int(inputs.get("issue_number")) - - event_name = _optional_str(ingress_event_name) or _optional_str( - payload.get("event_name") - ) - event_action = _optional_str(ingress_event_action) or _optional_str( - payload.get("action") - ) - - return RelayedGitHubContext( - event_name=event_name, - event_action=event_action, - pull_request_number=pull_request_number, - pull_request_title=pull_request_title, - pull_request_head_ref=pull_request_head_ref, - pull_request_head_sha=pull_request_head_sha, - issue_number=issue_number, - comment_id=_positive_int(comment.get("id")) - if isinstance(comment, dict) - else None, - discussion_number=_positive_int(discussion.get("number")) - if isinstance(discussion, dict) - else None, - ) - - -def build_relayed_context_instructions(ctx: RelayedGitHubContext) -> str: - if not ctx.has_actionable_target: - return "" - - lines = [ - "## Relayed ingress context (authoritative)", - "", - "This run reached the agent through the oblt-aw entrypoint (`workflow_dispatch` relay).", - "The built-in GitHub context block above may show empty PR/issue identifiers — ignore those empty values.", - "Use the relayed values below for MCP calls, analysis, comments, labels, and checkout.", - "", - ] - - if ctx.event_name: - lines.append(f"- **relay-event-name**: {ctx.event_name}") - if ctx.event_action: - lines.append(f"- **relay-event-action**: {ctx.event_action}") - if ctx.pull_request_number is not None: - lines.append(f"- **pull-request-number**: {ctx.pull_request_number}") - if ctx.pull_request_title: - lines.append(f"- **pull-request-title**: {ctx.pull_request_title}") - if ctx.pull_request_head_ref: - lines.append(f"- **pull-request-head-ref**: {ctx.pull_request_head_ref}") - if ctx.pull_request_head_sha: - lines.append(f"- **pull-request-head-sha**: {ctx.pull_request_head_sha}") - if ctx.issue_number is not None: - lines.append(f"- **issue-number**: {ctx.issue_number}") - if ctx.comment_id is not None: - lines.append(f"- **comment-id**: {ctx.comment_id}") - if ctx.discussion_number is not None: - lines.append(f"- **discussion-number**: {ctx.discussion_number}") - - lines.extend( - [ - "", - "Rules:", - "- Do not emit `missing_data` for pull request or issue numbers when relayed values above are present.", - "- Prefer GitHub MCP tools (`pull_request_read`, `issue_read`, and related methods) with these relayed numbers.", - "- Treat relayed pull-request/issue numbers as the workflow target even when built-in context shows `#` or blank values.", - ] - ) - return "\n".join(lines) - - -def build_relayed_setup_commands(ctx: RelayedGitHubContext) -> list[str]: - if ctx.pull_request_number is None or not ctx.pull_request_head_ref: - return [] - - ref = ctx.pull_request_head_ref - pr_number = ctx.pull_request_number - return [ - "set -euo pipefail", - ': "${GH_TOKEN:=${GITHUB_TOKEN:?GitHub token required for relayed PR checkout}}"', - "gh auth setup-git", - # Fetch to FETCH_HEAD only: refspec updates to the checked-out branch fail in CI. - f"git fetch origin {shlex.quote(f'pull/{pr_number}/head')}", - f"git checkout -B {shlex.quote(ref)} FETCH_HEAD", - ] - - -def apply_relayed_ingress_context( - payload_json: str, - additional_instructions: str, - setup_commands: list[str], - *, - ingress_event_name: str | None = None, - ingress_event_action: str | None = None, -) -> tuple[str, list[str]]: - payload = parse_relayed_ingress_payload(payload_json) - if payload is None: - return additional_instructions, setup_commands - - ctx = extract_relayed_github_context( - payload, - ingress_event_name=ingress_event_name, - ingress_event_action=ingress_event_action, - ) - relayed_block = build_relayed_context_instructions(ctx) - if not relayed_block: - return additional_instructions, setup_commands - - relayed_setup = build_relayed_setup_commands(ctx) - merged_setup = relayed_setup + setup_commands - - platform = additional_instructions.strip() - merged_additional = f"{relayed_block}\n\n{platform}" if platform else relayed_block - return merged_additional, merged_setup diff --git a/scripts/oblt_aw_route_specs.py b/scripts/oblt_aw_route_specs.py deleted file mode 100644 index fad03bbd..00000000 --- a/scripts/oblt_aw_route_specs.py +++ /dev/null @@ -1,521 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026-2027 Elasticsearch B.V. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -""" -Ingress dispatch metadata from per-org workflow-registry.json. - -Each ``workflows[]`` entry declares one dashboard ``id`` and an ``ingress_routes`` -array of objects. Each object may include ``id``, ``workflow_file``, and -``allowed_bot_users_from``. -""" - -from __future__ import annotations - -import json -import re -from dataclasses import dataclass -from pathlib import Path -from typing import Literal - -AllowedBotUsersFrom = Literal["", "allowed-pr", "allowed-issue"] -ALLOWED_BOT_USERS_FROM_VALUES: frozenset[str] = frozenset( - {"", "allowed-pr", "allowed-issue"} -) - - -class RegistryParseError(ValueError): - """Invalid workflow-registry.json route config metadata.""" - - -@dataclass(frozen=True) -class IngressRouteSpec: - route_id: str - workflow_file: str - allowed_bot_users_from: AllowedBotUsersFrom = "" - registry_workflow_id: str = "" - - -ORG_WORKFLOW_PREFIX = { - "obs": "oblt-aw", - "docs": "docs-aw", -} - - -def default_workflow_file(route_id: str, org_key: str = "obs") -> str: - """Default naming contract when workflow_file is omitted in the registry.""" - prefix = ORG_WORKFLOW_PREFIX.get(org_key, f"{org_key}-aw") - return f"{prefix}-{route_id}.yml" - - -def _normalize_route_entry(entry: object, *, context: str) -> dict[str, object]: - if not isinstance(entry, dict): - raise RegistryParseError(f"{context} must be an object") - return entry - - -def _parse_route_config_entry( - entry: object, - *, - context: str, - org_key: str, - registry_workflow_id: str, -) -> IngressRouteSpec: - normalized = _normalize_route_entry(entry, context=context) - - route_id = normalized.get("id") - if not isinstance(route_id, str) or not route_id: - raise RegistryParseError(f"{context} requires string 'id'") - - workflow_file = normalized.get("workflow_file") - if workflow_file is None: - workflow_file = default_workflow_file(route_id, org_key) - elif not isinstance(workflow_file, str) or not workflow_file.strip(): - raise RegistryParseError(f"{context}.workflow_file must be a non-empty string") - - allowed_raw = normalized.get("allowed_bot_users_from", "") - if allowed_raw is None: - allowed_raw = "" - if not isinstance(allowed_raw, str): - raise RegistryParseError(f"{context}.allowed_bot_users_from must be a string") - if allowed_raw not in ALLOWED_BOT_USERS_FROM_VALUES: - raise RegistryParseError( - f"{context}.allowed_bot_users_from must be one of " - f"{sorted(ALLOWED_BOT_USERS_FROM_VALUES)!r}, got {allowed_raw!r}" - ) - - return IngressRouteSpec( - route_id=route_id, - workflow_file=workflow_file, - allowed_bot_users_from=allowed_raw, # type: ignore[arg-type] - ) - - -def parse_workflow_ingress_routes( - workflow: dict[str, object], - *, - org_key: str, - context: str, -) -> list[IngressRouteSpec]: - """Resolve ingress route specs from one ``workflows[]`` registry entry.""" - registry_workflow_id = workflow.get("id") - if not isinstance(registry_workflow_id, str) or not registry_workflow_id: - raise RegistryParseError(f"{context} requires string 'id'") - - if workflow.get("control_plane_workflows") is not None: - raise RegistryParseError( - f"{context}: remove deprecated 'control_plane_workflows'; " - "control-plane workflow files are derived from 'ingress_routes'" - ) - if workflow.get("config") is not None: - raise RegistryParseError( - f"{context}: rename deprecated 'config' to 'ingress_routes'" - ) - - routes = workflow.get("ingress_routes") - if routes is None: - raise RegistryParseError(f"{context} missing required 'ingress_routes' array") - if not isinstance(routes, list) or not routes: - raise RegistryParseError(f"{context}.ingress_routes must be a non-empty array") - - specs: list[IngressRouteSpec] = [] - seen_route_ids: set[str] = set() - for route_index, entry in enumerate(routes): - route_context = f"{context}.ingress_routes[{route_index}]" - spec = _parse_route_config_entry( - entry, - context=route_context, - org_key=org_key, - registry_workflow_id=registry_workflow_id, - ) - if spec.route_id in seen_route_ids: - raise RegistryParseError( - f"{route_context}: duplicate route id {spec.route_id!r}" - ) - seen_route_ids.add(spec.route_id) - specs.append( - IngressRouteSpec( - route_id=spec.route_id, - workflow_file=spec.workflow_file, - allowed_bot_users_from=spec.allowed_bot_users_from, - registry_workflow_id=registry_workflow_id, - ) - ) - return specs - - -def load_ingress_route_specs(registry_path: Path) -> dict[str, IngressRouteSpec]: - """Flatten ``workflows[].ingress_routes`` from a workflow-registry.json file.""" - raw = json.loads(registry_path.read_text(encoding="utf-8")) - workflows = raw.get("workflows") - if not isinstance(workflows, list): - raise SystemExit(f"{registry_path}: 'workflows' must be a JSON array") - - org_key = registry_path.parent.name - specs: dict[str, IngressRouteSpec] = {} - for wf_index, workflow in enumerate(workflows): - if not isinstance(workflow, dict): - raise SystemExit( - f"{registry_path}: workflows[{wf_index}] must be an object" - ) - context = f"{registry_path}: workflows[{wf_index}]" - try: - route_specs = parse_workflow_ingress_routes( - workflow, - org_key=org_key, - context=context, - ) - except RegistryParseError as exc: - raise SystemExit(str(exc)) from exc - - for spec in route_specs: - if spec.route_id in specs: - raise SystemExit( - f"{registry_path}: duplicate ingress route id '{spec.route_id}'" - ) - specs[spec.route_id] = spec - - if raw.get("ingress_routes") is not None: - raise SystemExit( - f"{registry_path}: top-level 'ingress_routes' is deprecated; " - "use workflows[].ingress_routes instead" - ) - - return specs - - -def validate_all_org_registries(config_dir: Path) -> None: - """Ensure each org workflow-registry.json defines workflows[].ingress_routes.""" - for org_key in ORG_WORKFLOW_PREFIX: - load_ingress_route_specs(config_dir / org_key / "workflow-registry.json") - - -ROUTE_JOB_PATTERN = re.compile(r"^\s+route-([\w-]+):\s*$", re.MULTILINE) -ROUTE_JOB_BLOCK_PATTERN = re.compile( - r"^ route-([\w-]+):\n(.*?)(?=^ \S|\Z)", - re.MULTILINE | re.DOTALL, -) -PERMISSION_SCOPE_PATTERN = re.compile( - r"^ ([a-z0-9-]+): (read|write|none)\s*$", re.MULTILINE -) - -_PERMISSION_LEVELS: dict[str, int] = {"none": 0, "read": 1, "write": 2} - - -def _parse_permissions_blocks(workflow_text: str) -> list[dict[str, str]]: - blocks: list[dict[str, str]] = [] - in_block = False - current: dict[str, str] = {} - for line in workflow_text.splitlines(): - if re.match(r"^\s+permissions:\s*$", line): - in_block = True - current = {} - continue - if in_block: - match = re.match(r"^\s+([a-z0-9-]+):\s*(read|write|none)\s*$", line) - if match: - current[match.group(1)] = match.group(2) - continue - if line.strip() and not re.match(r"^\s{6,}", line): - in_block = False - if current: - blocks.append(current) - current = {} - if in_block and current: - blocks.append(current) - return blocks - - -def max_job_permissions(workflow_path: Path) -> dict[str, str]: - """Union of the highest scope per key across all job permissions blocks.""" - merged: dict[str, str] = {} - for block in _parse_permissions_blocks(workflow_path.read_text(encoding="utf-8")): - for scope, level in block.items(): - if ( - scope not in merged - or _PERMISSION_LEVELS[level] > _PERMISSION_LEVELS[merged[scope]] - ): - merged[scope] = level - return merged - - -def parse_route_job_permissions( - ingress_text: str, route_id: str -) -> dict[str, str] | None: - """Return permissions for route-{route_id}, or None when the block omits it.""" - for match in ROUTE_JOB_BLOCK_PATTERN.finditer(ingress_text): - if match.group(1) != route_id: - continue - scopes = PERMISSION_SCOPE_PATTERN.findall(match.group(2)) - if not scopes: - return None - return dict(scopes) - raise RegistryParseError(f"route-{route_id} job not found in ingress workflow") - - -def format_route_job_permissions(permissions: dict[str, str]) -> str: - lines = [" permissions:"] - for scope in sorted(permissions): - lines.append(f" {scope}: {permissions[scope]}") - return "\n".join(lines) - - -def union_permissions( - permission_maps: list[dict[str, str]], -) -> dict[str, str]: - merged: dict[str, str] = {} - for permission_map in permission_maps: - for scope, level in permission_map.items(): - if ( - scope not in merged - or _PERMISSION_LEVELS[level] > _PERMISSION_LEVELS[merged[scope]] - ): - merged[scope] = level - return merged - - -def _permissions_cover( - actual: dict[str, str] | None, required: dict[str, str] -) -> list[str]: - if actual is None: - return [f"missing job permissions (need {required})"] - errors: list[str] = [] - for scope, required_level in sorted(required.items()): - actual_level = actual.get(scope) - if actual_level is None: - errors.append(f"missing scope {scope!r} (need {required_level})") - continue - if _PERMISSION_LEVELS[actual_level] < _PERMISSION_LEVELS[required_level]: - errors.append(f"{scope}: {actual_level} is below required {required_level}") - return errors - - -def _permissions_exceed(actual: dict[str, str], required: dict[str, str]) -> list[str]: - errors: list[str] = [] - for scope, actual_level in sorted(actual.items()): - required_level = required.get(scope) - if required_level is None: - errors.append(f"unnecessary scope {scope!r} ({actual_level})") - continue - if _PERMISSION_LEVELS[actual_level] > _PERMISSION_LEVELS[required_level]: - errors.append(f"{scope}: {actual_level} exceeds required {required_level}") - return errors - - -JOB_BLOCK_PATTERN = re.compile( - r"^ ([\w-]+):\n(.*?)(?=^ [\w-]+:|^ unsupported|\Z)", - re.MULTILINE | re.DOTALL, -) -JOB_PERMISSIONS_PATTERN = re.compile( - r"^\s{4}permissions:\s*\n((?:\s{6}[a-z0-9-]+: (?:read|write)\s*\n)+)", - re.MULTILINE, -) -LOCAL_REUSABLE_USES_PATTERN = re.compile(r"uses: \./\.github/workflows/([\w-]+\.yml)") - -# Minimum job permissions for control-plane reusable callees (workflow_call). -LOCAL_REUSABLE_CALLEE_MIN: dict[str, dict[str, str]] = { - "aw-resolve-apm-assets.yml": {"contents": "read"}, - "get-enabled-workflows.yml": {"contents": "read", "issues": "read"}, - "load-allowed-authors.yml": {"contents": "read"}, -} - -CLIENT_ENTRYPOINTS: dict[str, tuple[str, str]] = { - "obs": ("oblt-aw-ingress.yml", "oblt-aw.yml"), - "docs": ("docs-aw-ingress.yml", "docs-aw.yml"), -} - - -def parse_job_permissions_from_block(job_block: str) -> dict[str, str] | None: - match = JOB_PERMISSIONS_PATTERN.search(job_block) - if not match: - return None - return dict(PERMISSION_SCOPE_PATTERN.findall(match.group(1))) - - -def union_ingress_route_permissions( - ingress_path: Path, *, specs: dict[str, IngressRouteSpec] -) -> dict[str, str]: - ingress_text = ingress_path.read_text(encoding="utf-8") - maps: list[dict[str, str]] = [] - for route_id in specs: - perms = parse_route_job_permissions(ingress_text, route_id) - if perms: - maps.append(perms) - return union_permissions(maps) - - -def parse_client_ingress_job_permissions(client_path: Path) -> dict[str, str] | None: - text = client_path.read_text(encoding="utf-8") - match = re.search( - r"^ ingress:\n(.*?)(?=^ [\w-]+:|\Z)", - text, - re.MULTILINE | re.DOTALL, - ) - if not match: - return None - return parse_job_permissions_from_block(match.group(1)) - - -def validate_ingress_route_job_permissions( - ingress_path: Path, - *, - specs: dict[str, IngressRouteSpec], - workflows_dir: Path, -) -> None: - """Route jobs must declare permissions covering each routed workflow.""" - ingress_text = ingress_path.read_text(encoding="utf-8") - for route_id, spec in sorted(specs.items()): - required = max_job_permissions(workflows_dir / spec.workflow_file) - if not required: - continue - actual = parse_route_job_permissions(ingress_text, route_id) - errors = _permissions_cover(actual, required) - errors.extend(_permissions_exceed(actual or {}, required)) - if errors: - raise SystemExit( - f"{ingress_path.name} route-{route_id}: " + "; ".join(errors) - ) - - -def validate_workflow_local_reusable_job_permissions(workflow_path: Path) -> None: - """workflow_call jobs that invoke local reusables must declare callee minimum scopes.""" - text = workflow_path.read_text(encoding="utf-8") - if "workflow_call:" not in text: - return - for match in JOB_BLOCK_PATTERN.finditer(text): - job_id, block = match.group(1), match.group(2) - uses_match = LOCAL_REUSABLE_USES_PATTERN.search(block) - if not uses_match: - continue - callee = uses_match.group(1) - required = LOCAL_REUSABLE_CALLEE_MIN.get(callee) - if not required: - continue - actual = parse_job_permissions_from_block(block) - errors = _permissions_cover(actual, required) - errors.extend(_permissions_exceed(actual or {}, required)) - if errors: - raise SystemExit( - f"{workflow_path.name} job {job_id} -> {callee}: " + "; ".join(errors) - ) - - -def validate_client_entrypoint_permissions( - *, - org_key: str, - config_dir: Path, - workflows_dir: Path, - template_dir: Path, -) -> None: - """Distributed entrypoint ingress job permissions must match the ingress route union.""" - ingress_name, client_name = CLIENT_ENTRYPOINTS[org_key] - ingress_path = workflows_dir / ingress_name - client_path = template_dir / org_key / ".github/workflows" / client_name - specs = load_ingress_route_specs(config_dir / org_key / "workflow-registry.json") - required = union_ingress_route_permissions(ingress_path, specs=specs) - actual = parse_client_ingress_job_permissions(client_path) - errors = _permissions_cover(actual, required) - errors.extend(_permissions_exceed(actual or {}, required)) - if errors: - raise SystemExit(f"{client_path}: ingress job " + "; ".join(errors)) - - -def validate_all_workflow_local_reusable_job_permissions(workflows_dir: Path) -> None: - for path in sorted(workflows_dir.glob("*.yml")): - validate_workflow_local_reusable_job_permissions(path) - - -def load_ingress_route_job_ids(ingress_path: Path) -> list[str]: - """Extract route ids from oblt-aw-ingress.yml route-* reusable jobs.""" - text = ingress_path.read_text(encoding="utf-8") - return ROUTE_JOB_PATTERN.findall(text) - - -def validate_org_ingress_registry( - org_key: str, - *, - config_dir: Path, - workflows_dir: Path, - ingress_path: Path, -) -> None: - """Org registry ingress_routes must match ingress route-* jobs and workflow files.""" - registry_path = config_dir / org_key / "workflow-registry.json" - specs = load_ingress_route_specs(registry_path) - ingress_route_ids = load_ingress_route_job_ids(ingress_path) - - missing_specs = [rid for rid in ingress_route_ids if rid not in specs] - if missing_specs: - raise SystemExit( - f"config/{org_key}/workflow-registry.json ingress_routes missing ids: " - + ", ".join(missing_specs) - ) - - extra_specs = [rid for rid in specs if rid not in ingress_route_ids] - if extra_specs: - raise SystemExit( - f"ingress_routes ids without route-* job in {ingress_path.name}: " - + ", ".join(extra_specs) - ) - - missing_files = [ - spec.workflow_file - for spec in specs.values() - if not (workflows_dir / spec.workflow_file).is_file() - ] - if missing_files: - raise SystemExit( - "Missing .github/workflows file(s) referenced by ingress_routes: " - + ", ".join(sorted(set(missing_files))) - ) - - validate_ingress_route_job_permissions( - ingress_path, - specs=specs, - workflows_dir=workflows_dir, - ) - for spec in specs.values(): - validate_workflow_local_reusable_job_permissions( - workflows_dir / spec.workflow_file - ) - - -def validate_obs_ingress_registry( - *, - config_dir: Path, - workflows_dir: Path, - ingress_path: Path, -) -> None: - """Obs registry ingress_routes must match ingress route-* jobs and workflow files.""" - validate_org_ingress_registry( - "obs", - config_dir=config_dir, - workflows_dir=workflows_dir, - ingress_path=ingress_path, - ) - - -def validate_docs_ingress_registry( - *, - config_dir: Path, - workflows_dir: Path, - ingress_path: Path, -) -> None: - """Docs registry ingress_routes must match ingress route-* jobs and workflow files.""" - validate_org_ingress_registry( - "docs", - config_dir=config_dir, - workflows_dir=workflows_dir, - ingress_path=ingress_path, - ) diff --git a/scripts/resolve_apm_agentic_assets.py b/scripts/resolve_apm_agentic_assets.py index 2fe6374f..544f4194 100644 --- a/scripts/resolve_apm_agentic_assets.py +++ b/scripts/resolve_apm_agentic_assets.py @@ -24,9 +24,6 @@ REPO_ROOT Repository root (default: cwd) PLATFORM_ADDITIONAL_INSTRUCTIONS Multiline platform baseline text PLATFORM_INPUTS_JSON JSON object of platform workflow_call inputs - INGRESS_EVENT_NAME Relayed github.event_name from ingress (optional) - INGRESS_EVENT_ACTION Relayed github.event.action from ingress (optional) - INGRESS_EVENT_PAYLOAD_JSON Relayed github.event JSON from ingress (optional) CONTROL_PLANE_CONFIG_DIR Optional path to config/ for registry validation """ @@ -40,7 +37,6 @@ from apm_agentic_assets import resolve_agentic_assets from common import append_multiline_github_output, write_outputs -from ingress_github_context import apply_relayed_ingress_context def main() -> int: @@ -92,21 +88,8 @@ def main() -> int: return 1 additional = resolved["additional_instructions"] - setup_commands = list(resolved["setup_commands"]) - - ingress_payload = os.environ.get("INGRESS_EVENT_PAYLOAD_JSON", "").strip() - if ingress_payload: - additional, setup_commands = apply_relayed_ingress_context( - ingress_payload, - additional, - setup_commands, - ingress_event_name=os.environ.get("INGRESS_EVENT_NAME", "").strip() or None, - ingress_event_action=os.environ.get("INGRESS_EVENT_ACTION", "").strip() - or None, - ) - inputs_json = json.dumps(resolved["inputs"], ensure_ascii=False) - setup_json = json.dumps(setup_commands, ensure_ascii=False) + setup_json = json.dumps(resolved["setup_commands"], ensure_ascii=False) write_outputs( { diff --git a/scripts/resolve_control_plane_workflow_id.py b/scripts/resolve_control_plane_workflow_id.py new file mode 100644 index 00000000..31e68014 --- /dev/null +++ b/scripts/resolve_control_plane_workflow_id.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright 2026-2027 Elasticsearch B.V. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Resolve a control-plane workflow filename to its compound dashboard id. + +Reads config//workflow-registry.json and writes compound-workflow-id to +GITHUB_OUTPUT when set, otherwise prints to stdout. +""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +from common import write_outputs +from workflow_registry import resolve_compound_id + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Resolve control-plane workflow file to org:workflow-id" + ) + parser.add_argument( + "control_plane_workflow", + help="Workflow basename under .github/workflows/ (for example oblt-aw-automerge.yml)", + ) + parser.add_argument( + "--config-dir", + type=Path, + default=Path("config"), + help="Config root containing per-org workflow-registry.json trees", + ) + args = parser.parse_args() + + try: + compound_id = resolve_compound_id(args.config_dir, args.control_plane_workflow) + except ValueError as exc: + print(str(exc), file=sys.stderr) + return 1 + + if os.getenv("GITHUB_OUTPUT"): + write_outputs({"compound-workflow-id": compound_id}) + else: + print(compound_id) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/resolve_repository_token_policy.py b/scripts/resolve_repository_token_policy.py new file mode 100644 index 00000000..6543eee1 --- /dev/null +++ b/scripts/resolve_repository_token_policy.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Copyright 2026-2027 Elasticsearch B.V. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +""" +Write the configured token-policy for a repository to GITHUB_OUTPUT. + +Reads per-org ``active-repositories.json`` under ``--config-dir``. When no +policy is configured for the repository, writes an empty ``token-policy`` value. +""" + +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +from common import lookup_repository_token_policy, write_outputs + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Resolve token-policy for a repository from active-repositories.json" + ) + parser.add_argument( + "--config-dir", + type=Path, + default=Path(os.environ.get("CONFIG_DIR", "config")), + help="Directory containing config// trees", + ) + parser.add_argument( + "--repository", + default=os.environ.get("TARGET_REPOSITORY", "").strip(), + help="Repository in owner/repo form (defaults to TARGET_REPOSITORY env)", + ) + args = parser.parse_args() + if not args.repository: + raise SystemExit("--repository or TARGET_REPOSITORY is required") + policy = lookup_repository_token_policy(args.config_dir, args.repository) + write_outputs({"token-policy": policy}) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/validate_aw_workflow_prelude.py b/scripts/validate_aw_workflow_prelude.py index f7371ddf..a2d930aa 100644 --- a/scripts/validate_aw_workflow_prelude.py +++ b/scripts/validate_aw_workflow_prelude.py @@ -15,10 +15,10 @@ # under the License. """ -Validate aw-prelude placement for control-plane workflows. +Validate local *-aw-* route workflows and registry coherence. -- *-aw-* wrappers (except ingress) must not call aw-prelude; ingress owns prelude. -- oblt-aw-ingress.yml and docs-aw-ingress.yml must call aw-prelude.yml and define route-* jobs. +Route reusables (oblt-aw-*, docs-aw-*) receive shared event context from +*-aw-event-* orchestrators and declare workflow_call input shared-proceed. """ from __future__ import annotations @@ -32,16 +32,17 @@ WORKFLOWS_DIR = pathlib.Path(".github/workflows") CONFIG_DIR = pathlib.Path("config") AW_WORKFLOW_PATTERN = re.compile(r".+-aw-.+\.ya?ml$") +EVENT_ORCHESTRATOR_PATTERN = re.compile(r".+-aw-event-.+\.ya?ml$") +ROUTE_PATTERN = re.compile(r"^(?:oblt|docs)-aw-.+\.ya?ml$") PRELUDE_USES = re.compile( r"uses:\s*\./\.github/workflows/aw-prelude\.ya?ml\b", re.MULTILINE, ) PRELUDE_JOB = re.compile(r"^\s+prelude:\s*$", re.MULTILINE) -INGRESS_FILES = ("oblt-aw-ingress.yml", "docs-aw-ingress.yml") -ROUTE_JOB_PATTERN = re.compile(r"^\s+route-[\w-]+:\s*$", re.MULTILINE) +SHARED_PROCEED_INPUT = re.compile(r"^\s+shared-proceed:\s*$", re.MULTILINE) -def list_workflow_files() -> list[pathlib.Path]: +def list_subject_workflows() -> list[pathlib.Path]: if not WORKFLOWS_DIR.is_dir(): raise SystemExit(f"Missing directory: {WORKFLOWS_DIR}") paths = sorted(WORKFLOWS_DIR.glob("*.yml")) + sorted(WORKFLOWS_DIR.glob("*.yaml")) @@ -50,86 +51,66 @@ def list_workflow_files() -> list[pathlib.Path]: for p in paths if AW_WORKFLOW_PATTERN.match(p.name) and p.name != "aw-prelude.yml" - and p.name not in INGRESS_FILES + and not EVENT_ORCHESTRATOR_PATTERN.match(p.name) and not p.name.startswith(("trg-", "trigger-")) ] -def list_aw_wrappers(paths: list[pathlib.Path]) -> list[pathlib.Path]: - return [p for p in paths if p.name not in INGRESS_FILES] - - -def list_subject_workflows() -> list[pathlib.Path]: - """Workflows subject to resolve-apm-assets validation (wrappers, not ingress).""" - return [ - p - for p in list_aw_wrappers(list_workflow_files()) - if p.name != "aw-resolve-apm-assets.yml" - ] - - -def validate_aw_wrapper_no_prelude(path: pathlib.Path) -> list[str]: +def validate_route(path: pathlib.Path) -> list[str]: text = path.read_text(encoding="utf-8") errors: list[str] = [] if PRELUDE_JOB.search(text): - errors.append( - f"{path}: must not define a prelude job (aw-prelude runs in ingress only)" - ) + errors.append(f"{path}: route workflows must not define a prelude job") if PRELUDE_USES.search(text): - errors.append( - f"{path}: must not call aw-prelude.yml (prelude and route gating run in ingress)" - ) + errors.append(f"{path}: route workflows must not call aw-prelude.yml") + if not SHARED_PROCEED_INPUT.search(text): + errors.append(f"{path}: must declare workflow_call input shared-proceed") return errors -def validate_ingress(path: pathlib.Path) -> list[str]: - text = path.read_text(encoding="utf-8") - errors: list[str] = [] - if not PRELUDE_JOB.search(text): - errors.append(f"{path}: missing job id 'prelude'") - if not PRELUDE_USES.search(text): - errors.append(f"{path}: must call './.github/workflows/aw-prelude.yml'") - if not ROUTE_JOB_PATTERN.search(text): - errors.append(f"{path}: missing route-* dispatch job(s)") - return errors +def validate_workflow(path: pathlib.Path) -> list[str]: + if ROUTE_PATTERN.match(path.name): + return validate_route(path) + return [] + + +def validate_registry_for_subjects(subject_workflow_names: set[str]) -> list[str]: + errors = validate_registry_against_workflows( + CONFIG_DIR, + WORKFLOWS_DIR, + subject_workflow_names, + ) + routes = {name for name in subject_workflow_names if ROUTE_PATTERN.match(name)} + filtered: list[str] = [] + for err in errors: + path_name = err.split(":", 1)[0].split("/")[-1] + if path_name in routes and "prelude must pass control-plane-workflow" in err: + continue + filtered.append(err) + return filtered def main() -> int: - paths = list_workflow_files() - if not paths: + errors: list[str] = [] + subjects = list_subject_workflows() + if not subjects: print("No *-aw-* workflows found to validate.", file=sys.stderr) return 1 - wrappers = list_aw_wrappers(paths) - errors: list[str] = [] - for path in wrappers: - errors.extend(validate_aw_wrapper_no_prelude(path)) - - for ingress_name in INGRESS_FILES: - ingress = WORKFLOWS_DIR / ingress_name - if ingress.is_file(): - errors.extend(validate_ingress(ingress)) - else: - errors.append(f"{ingress}: missing ingress workflow") - - registry_subjects = {path.name for path in wrappers} - errors.extend( - validate_registry_against_workflows( - CONFIG_DIR, - WORKFLOWS_DIR, - registry_subjects, - ) - ) + for path in subjects: + errors.extend(validate_workflow(path)) + + errors.extend(validate_registry_for_subjects({path.name for path in subjects})) if errors: - print("aw-prelude enforcement failed:", file=sys.stderr) + print("aw workflow route validation failed:", file=sys.stderr) for err in errors: print(f" - {err}", file=sys.stderr) return 1 print( - f"Validated {len(wrappers)} *-aw wrapper(s) and " - f"{len(INGRESS_FILES)} ingress workflow(s)." + f"Validated {len(subjects)} *-aw-* workflow(s): " + "routes declare shared-proceed; event orchestrators call aw-prelude.yml." ) return 0 diff --git a/scripts/validate_ingress_registry.py b/scripts/validate_ingress_registry.py deleted file mode 100644 index c311a004..00000000 --- a/scripts/validate_ingress_registry.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026-2027 Elasticsearch B.V. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -"""Validate per-org ingress_routes in workflow-registry.json against ingress route jobs.""" - -from __future__ import annotations - -from pathlib import Path - -from oblt_aw_route_specs import ( - validate_all_org_registries, - validate_all_workflow_local_reusable_job_permissions, - validate_client_entrypoint_permissions, - validate_docs_ingress_registry, - validate_obs_ingress_registry, -) - -CONFIG_DIR = Path("config") -WORKFLOWS_DIR = Path(".github/workflows") -TEMPLATE_DIR = Path(".github/remote-workflow-template") - - -def main() -> int: - validate_all_org_registries(CONFIG_DIR) - validate_obs_ingress_registry( - config_dir=CONFIG_DIR, - workflows_dir=WORKFLOWS_DIR, - ingress_path=WORKFLOWS_DIR / "oblt-aw-ingress.yml", - ) - validate_docs_ingress_registry( - config_dir=CONFIG_DIR, - workflows_dir=WORKFLOWS_DIR, - ingress_path=WORKFLOWS_DIR / "docs-aw-ingress.yml", - ) - validate_all_workflow_local_reusable_job_permissions(WORKFLOWS_DIR) - for org_key in ("obs", "docs"): - validate_client_entrypoint_permissions( - org_key=org_key, - config_dir=CONFIG_DIR, - workflows_dir=WORKFLOWS_DIR, - template_dir=TEMPLATE_DIR, - ) - print("Ingress registry validated.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/workflow_registry.py b/scripts/workflow_registry.py index a3efbc80..82e9770b 100644 --- a/scripts/workflow_registry.py +++ b/scripts/workflow_registry.py @@ -18,22 +18,20 @@ Load and validate per-org workflow-registry.json files. Each workflow entry maps a dashboard ``id`` to one or more control-plane reusable -workflow files under ``.github/workflows/``, derived from ``ingress_routes``. +workflow files under ``.github/workflows/`` via ``control_plane_workflows``. """ from __future__ import annotations import json import re -import sys from dataclasses import dataclass from pathlib import Path from common import compound_workflow_key, discover_org_config_dirs -from oblt_aw_route_specs import RegistryParseError, parse_workflow_ingress_routes CONTROL_PLANE_WORKFLOW_NAME = re.compile(r"^[a-z0-9-]+-aw-[a-z0-9-]+\.ya?ml$") -LEGACY_CONTROL_PLANE_WORKFLOW = re.compile( +PRELUDE_CONTROL_PLANE_WORKFLOW = re.compile( r"control-plane-workflow:\s*([^\s#]+)", re.MULTILINE, ) @@ -81,23 +79,18 @@ def parse_registry_entries(org_dir: Path) -> list[RegistryWorkflowEntry]: f"{org_dir}: workflows[{index}].id must be a non-empty string" ) - context = f"{org_dir}: workflows[{index}]" - try: - route_specs = parse_workflow_ingress_routes( - item, - org_key=org_key, - context=context, + files = item.get("control_plane_workflows") + if not isinstance(files, list) or not files: + raise ValueError( + f"{org_dir}: workflows[{index}] ({workflow_id!r}) must define a " + "non-empty control_plane_workflows array" ) - except RegistryParseError as exc: - raise ValueError(str(exc)) from exc - normalized: list[str] = [] - for route_spec in route_specs: - name = route_spec.workflow_file - if not CONTROL_PLANE_WORKFLOW_NAME.match(name): + for file_index, name in enumerate(files): + if not isinstance(name, str) or not CONTROL_PLANE_WORKFLOW_NAME.match(name): raise ValueError( - f"{org_dir}: workflows[{index}] ingress route " - f"{route_spec.route_id!r} must resolve to *-aw-*.yml, got {name!r}" + f"{org_dir}: workflows[{index}].control_plane_workflows[{file_index}] " + f"must match *-aw-*.yml, got {name!r}" ) normalized.append(name) entries.append( @@ -110,19 +103,6 @@ def parse_registry_entries(org_dir: Path) -> list[RegistryWorkflowEntry]: return entries -def resolve_compound_id(config_dir: Path, control_plane_workflow: str) -> str: - """Map a control-plane workflow basename to its compound dashboard id.""" - index = build_control_plane_workflow_index(config_dir) - entry = index.get(control_plane_workflow) - if entry is None: - known = ", ".join(sorted(index)) - raise ValueError( - f"control-plane workflow {control_plane_workflow!r} is not listed in any " - f"workflow-registry.json ingress_routes (known: {known})" - ) - return entry.compound_id - - def build_control_plane_workflow_index( config_dir: Path, ) -> dict[str, RegistryWorkflowEntry]: @@ -134,7 +114,7 @@ def build_control_plane_workflow_index( if filename in index: previous = index[filename] raise ValueError( - f"ingress route workflow {filename!r} is listed under both " + f"control_plane_workflows[{filename!r}] is listed under both " f"{previous.org_key}:{previous.workflow_id} and " f"{entry.org_key}:{entry.workflow_id}" ) @@ -142,6 +122,18 @@ def build_control_plane_workflow_index( return index +def resolve_compound_id(config_dir: Path, control_plane_workflow: str) -> str: + index = build_control_plane_workflow_index(config_dir) + entry = index.get(control_plane_workflow) + if entry is None: + known = ", ".join(sorted(index)) + raise ValueError( + f"control-plane workflow {control_plane_workflow!r} is not listed in any " + f"workflow-registry.json control_plane_workflows (known: {known})" + ) + return entry.compound_id + + def validate_registry_against_workflows( config_dir: Path, workflows_dir: Path, @@ -160,7 +152,7 @@ def validate_registry_against_workflows( for name in sorted(missing): errors.append( f"{workflows_dir / name}: not listed in any " - "workflow-registry.json ingress_routes" + "workflow-registry.json control_plane_workflows" ) stale = registered - subject_workflow_names @@ -178,57 +170,20 @@ def validate_registry_against_workflows( text = path.read_text(encoding="utf-8") if LEGACY_ENABLED_WORKFLOW_ID.search(text): errors.append( - f"{path}: remove enabled-workflow-id " - "(dashboard gating is enforced in ingress route jobs)" + f"{path}: use control-plane-workflow instead of enabled-workflow-id " + "(compound id is resolved from workflow-registry.json)" ) - # control-plane-workflow is required on aw-resolve-apm-assets calls; only - # forbid legacy passes to aw-prelude. - if re.search( - r"uses:\s*\./\.github/workflows/aw-prelude\.ya?ml[\s\S]{0,800}?" - r"control-plane-workflow:", - text, - ): + continue + match = PRELUDE_CONTROL_PLANE_WORKFLOW.search(text) + if not match: + errors.append( + f"{path}: prelude must pass control-plane-workflow matching this file" + ) + continue + declared = match.group(1) + if declared != path_name: errors.append( - f"{path}: remove control-plane-workflow from aw-prelude " - "(prelude runs in ingress without per-wrapper gating)" + f"{path}: control-plane-workflow is {declared!r}, expected {path_name!r}" ) + continue return errors - - -def main() -> int: - """CLI: resolve a control-plane workflow file to org:workflow-id.""" - import argparse - import os - - from common import write_outputs - - parser = argparse.ArgumentParser( - description="Resolve control-plane workflow file to org:workflow-id" - ) - parser.add_argument( - "control_plane_workflow", - help="Workflow basename under .github/workflows/ (for example oblt-aw-automerge.yml)", - ) - parser.add_argument( - "--config-dir", - type=Path, - default=Path("config"), - help="Config root containing per-org workflow-registry.json trees", - ) - args = parser.parse_args() - - try: - compound_id = resolve_compound_id(args.config_dir, args.control_plane_workflow) - except ValueError as exc: - print(str(exc), file=sys.stderr) - return 1 - - if os.getenv("GITHUB_OUTPUT"): - write_outputs({"compound-workflow-id": compound_id}) - else: - print(compound_id) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/test_build_repos_matrix.py b/tests/test_build_repos_matrix.py index ae87fa7d..f74d4c74 100644 --- a/tests/test_build_repos_matrix.py +++ b/tests/test_build_repos_matrix.py @@ -81,29 +81,35 @@ def test_repositories_not_a_list_raises(self) -> None: with pytest.raises(SystemExit, match=r"`repositories` must be a list"): parse_repositories(content) - def test_object_entry_with_repository_key(self) -> None: + def test_object_entry_with_token_policy(self) -> None: content = json.dumps( { "repositories": [ "elastic/foo", - {"repository": "elastic/bar"}, + { + "repository": "elastic/bar", + "token-policy": "token-policy-abc123", + }, ] } ) entries = parse_active_repository_entries(content) - assert [e.repository for e in entries] == ["elastic/bar", "elastic/foo"] + assert [(e.repository, e.token_policy) for e in entries] == [ + ("elastic/bar", "token-policy-abc123"), + ("elastic/foo", ""), + ] - def test_duplicate_repo_entries_deduplicated(self) -> None: + def test_duplicate_repo_conflicting_policy_raises(self) -> None: content = json.dumps( { "repositories": [ - {"repository": "elastic/foo"}, - "elastic/foo", + {"repository": "elastic/foo", "token-policy": "token-policy-a"}, + {"repository": "elastic/foo", "token-policy": "token-policy-b"}, ] } ) - entries = parse_active_repository_entries(content) - assert [e.repository for e in entries] == ["elastic/foo"] + with pytest.raises(SystemExit, match="conflicting token-policy"): + parse_active_repository_entries(content) # ── write_outputs ────────────────────────────────────────────────────────────── @@ -204,4 +210,59 @@ def test_with_active_repositories_builds_matrix( assert len(matrix) == 2 repos = {m["repository"] for m in matrix} assert repos == {"elastic/foo", "elastic/bar"} - assert all(set(entry) == {"repository"} for entry in matrix) + assert all(m["token-policy"] == "" for m in matrix) + + def test_matrix_includes_configured_token_policy( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path + ) -> None: + import importlib.util + + output_file = tmp_path / "github_output" + output_file.touch() + monkeypatch.setenv("GITHUB_OUTPUT", str(output_file)) + (tmp_path / "scripts").mkdir(parents=True) + script_src = pathlib.Path(brm.__file__).read_text() + (tmp_path / "scripts" / "build_repos_matrix.py").write_text(script_src) + (tmp_path / "scripts" / "common.py").write_text( + pathlib.Path(__file__) + .parent.parent.joinpath("scripts", "common.py") + .read_text() + ) + (tmp_path / "config" / "obs").mkdir(parents=True) + (tmp_path / "config" / "obs" / "workflow-registry.json").write_text( + json.dumps({"workflows": []}) + ) + (tmp_path / "config" / "obs" / "active-repositories.json").write_text( + json.dumps( + { + "repositories": [ + { + "repository": "elastic/foo", + "token-policy": "token-policy-custom", + } + ] + } + ) + ) + + spec = importlib.util.spec_from_file_location( + "brm_test", + tmp_path / "scripts" / "build_repos_matrix.py", + ) + assert spec and spec.loader + brm_test = importlib.util.module_from_spec(spec) + spec.loader.exec_module(brm_test) + + assert brm_test.main() == 0 + repos_line = next( + line + for line in output_file.read_text().splitlines() + if line.startswith("repos=") + ) + matrix = json.loads(repos_line.split("=", 1)[1]) + assert matrix == [ + { + "repository": "elastic/foo", + "token-policy": "token-policy-custom", + } + ] diff --git a/tests/test_build_target_operations.py b/tests/test_build_target_operations.py index 12cfa209..3f3a5949 100644 --- a/tests/test_build_target_operations.py +++ b/tests/test_build_target_operations.py @@ -205,8 +205,7 @@ def _setup_env( / "workflows" ) tmpl.mkdir(parents=True, exist_ok=True) - (tmpl / "trigger-oblt-aw.yml").write_text("name: client\n") - (tmpl / "oblt-aw.yml").write_text("name: entrypoint\n") + (tmpl / "trigger-oblt-aw-pull-request.yml").write_text("name: client\n") return output_file def test_no_changes_skips_work( @@ -243,8 +242,7 @@ def test_with_changes_builds_targets( assert isinstance(t["files"], list) assert len(t["files"]) >= 1 dsts = {f["dst"] for f in t["files"]} - assert ".github/workflows/trigger-oblt-aw.yml" in dsts - assert ".github/workflows/oblt-aw.yml" in dsts + assert ".github/workflows/trigger-oblt-aw-pull-request.yml" in dsts assert "remove_files" in t assert t["remove_files"] == [] @@ -334,7 +332,7 @@ def fake_list_at_ref(org_key: str, ref: str) -> list[dict[str, str]]: install = next(t for t in targets if t["repository"] == "elastic/foo") assert ".github/workflows/trg-oblt-aw-automerge.yml" in install["remove_files"] dsts = {f["dst"] for f in install["files"]} - assert ".github/workflows/trigger-oblt-aw.yml" in dsts + assert ".github/workflows/trigger-oblt-aw-pull-request.yml" in dsts def test_install_includes_remove_files_for_dropped_templates( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path @@ -359,8 +357,8 @@ def fake_list_at_ref(org_key: str, ref: str) -> list[dict[str, str]]: "dst": ".github/workflows/oblt-aw.yml", }, { - "src": ".github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-automerge.yml", - "dst": ".github/workflows/trigger-oblt-aw-automerge.yml", + "src": ".github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-pull-request.yml", + "dst": ".github/workflows/trigger-oblt-aw-pull-request.yml", }, ] @@ -379,9 +377,6 @@ def fake_list_at_ref(org_key: str, ref: str) -> list[dict[str, str]]: ) install = next(t for t in targets if t["repository"] == "elastic/foo") assert install["operation"] == "install" - assert ( - ".github/workflows/trigger-oblt-aw-automerge.yml" in install["remove_files"] - ) + assert ".github/workflows/oblt-aw.yml" in install["remove_files"] dsts = {f["dst"] for f in install["files"]} - assert ".github/workflows/trigger-oblt-aw.yml" in dsts - assert ".github/workflows/oblt-aw.yml" in dsts + assert ".github/workflows/trigger-oblt-aw-pull-request.yml" in dsts diff --git a/tests/test_evaluate_workflow_gates.py b/tests/test_evaluate_workflow_gates.py new file mode 100644 index 00000000..8caaa0c9 --- /dev/null +++ b/tests/test_evaluate_workflow_gates.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# Copyright 2026-2027 Elasticsearch B.V. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +import json +import pathlib +import sys + +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +from evaluate_workflow_gates import evaluate_gates # noqa: E402 + + +@pytest.fixture +def config_dir(tmp_path: pathlib.Path) -> pathlib.Path: + obs = tmp_path / "config" / "obs" + obs.mkdir(parents=True) + registry = { + "workflows": [ + { + "id": "automerge", + "control_plane_workflows": ["oblt-aw-automerge.yml"], + }, + { + "id": "dependency-review", + "control_plane_workflows": ["oblt-aw-dependency-review.yml"], + }, + ] + } + (obs / "workflow-registry.json").write_text(json.dumps(registry), encoding="utf-8") + (obs / "active-repositories.json").write_text( + json.dumps({"repositories": []}), encoding="utf-8" + ) + return tmp_path / "config" + + +def test_all_enabled_when_no_dashboard(config_dir: pathlib.Path) -> None: + result = evaluate_gates( + config_dir, + ["oblt-aw-automerge.yml", "oblt-aw-dependency-review.yml"], + effective_raw="", + enabled_workflows_json="[]", + ) + assert result == { + "oblt-aw-automerge.yml": "true", + "oblt-aw-dependency-review.yml": "true", + } + + +def test_selective_enablement(config_dir: pathlib.Path) -> None: + enabled = json.dumps(["obs:automerge"]) + result = evaluate_gates( + config_dir, + ["oblt-aw-automerge.yml", "oblt-aw-dependency-review.yml"], + effective_raw="checked", + enabled_workflows_json=enabled, + ) + assert result["oblt-aw-automerge.yml"] == "true" + assert result["oblt-aw-dependency-review.yml"] == "false" + + +def test_unknown_workflow_raises(config_dir: pathlib.Path) -> None: + with pytest.raises(ValueError, match="not listed"): + evaluate_gates( + config_dir, + ["oblt-aw-unknown.yml"], + effective_raw="", + enabled_workflows_json="[]", + ) diff --git a/tests/test_ingress_github_context.py b/tests/test_ingress_github_context.py deleted file mode 100644 index 1336d0dc..00000000 --- a/tests/test_ingress_github_context.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Unit tests for scripts/ingress_github_context.py""" - -from __future__ import annotations - -import json -import pathlib -import sys - -_root = pathlib.Path(__file__).parent.parent -sys.path.insert(0, str(_root / "scripts")) - -import ingress_github_context as igc # noqa: E402 - - -def test_parse_relayed_payload_accepts_double_encoded_json() -> None: - payload = { - "action": "synchronize", - "pull_request": {"number": 42, "title": "Fix deps"}, - } - raw = json.dumps(json.dumps(payload)) - parsed = igc.parse_relayed_ingress_payload(raw) - assert parsed == payload - - -def test_extract_pull_request_context_from_relayed_event() -> None: - payload = { - "action": "synchronize", - "pull_request": { - "number": 123, - "title": "Update lodash", - "head": {"ref": "dependabot/npm/lodash", "sha": "abc123"}, - }, - } - ctx = igc.extract_relayed_github_context(payload) - assert ctx.pull_request_number == 123 - assert ctx.pull_request_title == "Update lodash" - assert ctx.pull_request_head_ref == "dependabot/npm/lodash" - assert ctx.event_action == "synchronize" - - -def test_extract_issue_context_from_pr_issue_object() -> None: - payload = { - "action": "created", - "issue": {"number": 99, "title": "Docs", "pull_request": {}}, - } - ctx = igc.extract_relayed_github_context( - payload, ingress_event_name="issue_comment" - ) - assert ctx.pull_request_number == 99 - assert ctx.issue_number == 99 - - -def test_apply_relayed_context_prepends_instructions_and_checkout() -> None: - payload = { - "action": "opened", - "pull_request": { - "number": 7, - "title": "Test", - "head": {"ref": "feature/test", "sha": "deadbeef"}, - }, - } - additional, setup = igc.apply_relayed_ingress_context( - json.dumps(payload), - "Platform rules", - ["npm ci"], - ingress_event_name="pull_request", - ingress_event_action="opened", - ) - assert "## Relayed ingress context (authoritative)" in additional - assert "- **pull-request-number**: 7" in additional - assert additional.index("Relayed ingress context") < additional.index( - "Platform rules" - ) - assert setup[0] == "set -euo pipefail" - assert setup[1].startswith(': "${GH_TOKEN:=${GITHUB_TOKEN') - assert setup[2] == "gh auth setup-git" - assert setup[3] == "git fetch origin pull/7/head" - assert setup[4] == "git checkout -B feature/test FETCH_HEAD" - assert setup[-1] == "npm ci" - - -def test_build_relayed_setup_commands_uses_fetch_head_for_checked_out_branch() -> None: - ctx = igc.RelayedGitHubContext( - event_name="pull_request", - event_action="synchronize", - pull_request_number=55, - pull_request_title="Bump terragrunt", - pull_request_head_ref="updatecli_main_terragrunt/version", - pull_request_head_sha="abc123", - issue_number=None, - comment_id=None, - discussion_number=None, - ) - setup = igc.build_relayed_setup_commands(ctx) - assert setup[3] == "git fetch origin pull/55/head" - assert setup[4] == "git checkout -B updatecli_main_terragrunt/version FETCH_HEAD" - assert ":updatecli_main_terragrunt/version" not in setup[3] - - -def test_apply_relayed_context_noop_without_target() -> None: - additional, setup = igc.apply_relayed_ingress_context("{}", "Platform rules", []) - assert additional == "Platform rules" - assert setup == [] diff --git a/tests/test_oblt_aw_route_specs.py b/tests/test_oblt_aw_route_specs.py deleted file mode 100644 index 4021e48c..00000000 --- a/tests/test_oblt_aw_route_specs.py +++ /dev/null @@ -1,339 +0,0 @@ -"""Tests for scripts/oblt_aw_route_specs.py.""" - -from __future__ import annotations - -import json -import pathlib -import sys - -import pytest - -sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "scripts")) - -from oblt_aw_route_specs import ( # noqa: E402 - IngressRouteSpec, - default_workflow_file, - load_ingress_route_job_ids, - load_ingress_route_specs, - max_job_permissions, - parse_route_job_permissions, - union_ingress_route_permissions, - validate_all_org_registries, - validate_client_entrypoint_permissions, - validate_docs_ingress_registry, - validate_ingress_route_job_permissions, - validate_obs_ingress_registry, - validate_workflow_local_reusable_job_permissions, -) - - -def _write_registry( - tmp_path: pathlib.Path, - workflows: list[dict], - *, - org_key: str = "obs", - legacy_top_level: list[dict] | None = None, -) -> pathlib.Path: - payload: dict = {"workflows": workflows} - if legacy_top_level is not None: - payload["ingress_routes"] = legacy_top_level - org_dir = tmp_path / org_key - org_dir.mkdir(parents=True, exist_ok=True) - path = org_dir / "workflow-registry.json" - path.write_text(json.dumps(payload), encoding="utf-8") - return path - - -class TestDefaultWorkflowFile: - def test_obs_naming_contract(self) -> None: - assert default_workflow_file("automerge", "obs") == "oblt-aw-automerge.yml" - - def test_docs_naming_contract(self) -> None: - assert default_workflow_file("ai-menu", "docs") == "docs-aw-ai-menu.yml" - - -class TestLoadIngressRouteSpecs: - def test_loads_nested_ingress_routes(self, tmp_path: pathlib.Path) -> None: - path = _write_registry( - tmp_path, - [ - { - "id": "automerge", - "ingress_routes": [ - { - "id": "automerge", - "allowed_bot_users_from": "allowed-pr", - } - ], - }, - { - "id": "issue-triage", - "ingress_routes": [{"id": "issue-triage"}], - }, - ], - ) - specs = load_ingress_route_specs(path) - assert specs["automerge"].allowed_bot_users_from == "allowed-pr" - assert specs["automerge"].registry_workflow_id == "automerge" - assert specs["issue-triage"].workflow_file == "oblt-aw-issue-triage.yml" - - def test_flattens_multiple_routes_under_one_registry_workflow( - self, tmp_path: pathlib.Path - ) -> None: - path = _write_registry( - tmp_path, - [ - { - "id": "security", - "ingress_routes": [ - {"id": "security-detector"}, - { - "id": "security-triage", - "allowed_bot_users_from": "allowed-issue", - }, - ], - } - ], - ) - specs = load_ingress_route_specs(path) - assert specs["security-detector"].registry_workflow_id == "security" - assert specs["security-triage"].allowed_bot_users_from == "allowed-issue" - - def test_defaults_route_id_from_explicit_object( - self, tmp_path: pathlib.Path - ) -> None: - path = _write_registry( - tmp_path, - [{"id": "autodoc", "ingress_routes": [{"id": "autodoc"}]}], - ) - specs = load_ingress_route_specs(path) - assert specs["autodoc"].workflow_file == "oblt-aw-autodoc.yml" - assert specs["autodoc"].registry_workflow_id == "autodoc" - - def test_rejects_string_ingress_route_entries(self, tmp_path: pathlib.Path) -> None: - path = _write_registry( - tmp_path, - [{"id": "autodoc", "ingress_routes": ["autodoc"]}], - ) - with pytest.raises(SystemExit, match="must be an object"): - load_ingress_route_specs(path) - - def test_rejects_missing_ingress_routes(self, tmp_path: pathlib.Path) -> None: - path = _write_registry(tmp_path, [{"id": "autodoc"}]) - with pytest.raises(SystemExit, match="missing required 'ingress_routes'"): - load_ingress_route_specs(path) - - def test_rejects_legacy_config_key(self, tmp_path: pathlib.Path) -> None: - path = _write_registry( - tmp_path, - [{"id": "autodoc", "config": [{}]}], - ) - with pytest.raises( - SystemExit, match="rename deprecated 'config' to 'ingress_routes'" - ): - load_ingress_route_specs(path) - - def test_rejects_legacy_control_plane_workflows( - self, tmp_path: pathlib.Path - ) -> None: - path = _write_registry( - tmp_path, - [ - { - "id": "autodoc", - "control_plane_workflows": ["oblt-aw-autodoc.yml"], - } - ], - ) - with pytest.raises(SystemExit, match="control_plane_workflows"): - load_ingress_route_specs(path) - - def test_rejects_top_level_ingress_routes(self, tmp_path: pathlib.Path) -> None: - path = _write_registry( - tmp_path, - [{"id": "autodoc", "ingress_routes": [{"id": "autodoc"}]}], - legacy_top_level=[{"id": "autodoc"}], - ) - with pytest.raises(SystemExit, match="deprecated"): - load_ingress_route_specs(path) - - def test_rejects_empty_ingress_routes_on_workflow( - self, tmp_path: pathlib.Path - ) -> None: - path = _write_registry(tmp_path, [{"id": "autodoc", "ingress_routes": []}]) - with pytest.raises(SystemExit, match="non-empty array"): - load_ingress_route_specs(path) - - -class TestIngressRoutePermissions: - def test_max_job_permissions_unions_job_scopes( - self, tmp_path: pathlib.Path - ) -> None: - workflow = tmp_path / "oblt-aw-sample.yml" - workflow.write_text( - "permissions:\n contents: read\n\n" - "jobs:\n" - " one:\n" - " permissions:\n" - " issues: read\n" - " two:\n" - " permissions:\n" - " issues: write\n" - " pull-requests: read\n", - encoding="utf-8", - ) - assert max_job_permissions(workflow) == { - "issues": "write", - "pull-requests": "read", - } - - def test_parse_route_job_permissions(self, tmp_path: pathlib.Path) -> None: - ingress = tmp_path / "ingress.yml" - ingress.write_text( - "jobs:\n" - " route-sample:\n" - " needs: prelude\n" - " permissions:\n" - " issues: write\n" - " uses: ./.github/workflows/oblt-aw-sample.yml\n", - encoding="utf-8", - ) - text = ingress.read_text(encoding="utf-8") - assert parse_route_job_permissions(text, "sample") == {"issues": "write"} - - def test_validate_ingress_route_job_permissions_fails_when_over_granted( - self, tmp_path: pathlib.Path - ) -> None: - workflows_dir = tmp_path / "workflows" - workflows_dir.mkdir() - (workflows_dir / "oblt-aw-sample.yml").write_text( - "jobs:\n run:\n permissions:\n issues: write\n", - encoding="utf-8", - ) - ingress = tmp_path / "oblt-aw-ingress.yml" - ingress.write_text( - "jobs:\n" - " route-sample:\n" - " needs: prelude\n" - " permissions:\n" - " issues: write\n" - " pull-requests: write\n" - " uses: ./.github/workflows/oblt-aw-sample.yml\n", - encoding="utf-8", - ) - specs = load_ingress_route_specs( - _write_registry( - tmp_path, - [{"id": "sample", "ingress_routes": [{"id": "sample"}]}], - ) - ) - with pytest.raises(SystemExit, match="pull-requests"): - validate_ingress_route_job_permissions( - ingress, - specs=specs, - workflows_dir=workflows_dir, - ) - - def test_validate_workflow_local_reusable_job_permissions_requires_explicit( - self, tmp_path: pathlib.Path - ) -> None: - workflow = tmp_path / "oblt-aw-sample.yml" - workflow.write_text( - "on:\n workflow_call:\njobs:\n" - " resolve-apm-assets:\n" - " uses: ./.github/workflows/aw-resolve-apm-assets.yml\n", - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="missing job permissions"): - validate_workflow_local_reusable_job_permissions(workflow) - - def test_union_ingress_route_permissions(self, tmp_path: pathlib.Path) -> None: - ingress = tmp_path / "oblt-aw-ingress.yml" - ingress.write_text( - "jobs:\n" - " route-a:\n permissions:\n issues: write\n" - " uses: ./.github/workflows/oblt-aw-a.yml\n" - " route-b:\n permissions:\n pull-requests: read\n" - " uses: ./.github/workflows/oblt-aw-b.yml\n", - encoding="utf-8", - ) - specs = { - "a": IngressRouteSpec(route_id="a", workflow_file="oblt-aw-a.yml"), - "b": IngressRouteSpec(route_id="b", workflow_file="oblt-aw-b.yml"), - } - union = union_ingress_route_permissions(ingress, specs=specs) - assert union == {"issues": "write", "pull-requests": "read"} - - def test_validate_ingress_route_job_permissions_fails_when_missing( - self, tmp_path: pathlib.Path - ) -> None: - workflows_dir = tmp_path / "workflows" - workflows_dir.mkdir() - (workflows_dir / "oblt-aw-sample.yml").write_text( - "jobs:\n run:\n permissions:\n issues: write\n", - encoding="utf-8", - ) - ingress = tmp_path / "oblt-aw-ingress.yml" - ingress.write_text( - "jobs:\n" - " route-sample:\n" - " needs: prelude\n" - " uses: ./.github/workflows/oblt-aw-sample.yml\n", - encoding="utf-8", - ) - specs = load_ingress_route_specs( - _write_registry( - tmp_path, - [{"id": "sample", "ingress_routes": [{"id": "sample"}]}], - ) - ) - with pytest.raises(SystemExit, match="missing job permissions"): - validate_ingress_route_job_permissions( - ingress, - specs=specs, - workflows_dir=workflows_dir, - ) - - -class TestLoadIngressRouteJobIds: - def test_extracts_route_job_ids(self, tmp_path: pathlib.Path) -> None: - ingress = tmp_path / "oblt-aw-ingress.yml" - ingress.write_text( - "jobs:\n" - " route-automerge:\n uses: ./.github/workflows/oblt-aw-automerge.yml\n" - " route-issue-triage:\n uses: ./.github/workflows/oblt-aw-issue-triage.yml\n", - encoding="utf-8", - ) - assert load_ingress_route_job_ids(ingress) == ["automerge", "issue-triage"] - - -class TestRepoRegistryValidation: - def test_obs_registry_in_repo(self) -> None: - repo_root = pathlib.Path(__file__).parent.parent - validate_all_org_registries(repo_root / "config") - - def test_obs_registry_matches_ingress_jobs_and_workflow_files(self) -> None: - repo_root = pathlib.Path(__file__).parent.parent - validate_obs_ingress_registry( - config_dir=repo_root / "config", - workflows_dir=repo_root / ".github" / "workflows", - ingress_path=repo_root / ".github" / "workflows" / "oblt-aw-ingress.yml", - ) - - def test_docs_registry_matches_ingress_jobs_and_workflow_files(self) -> None: - repo_root = pathlib.Path(__file__).parent.parent - validate_docs_ingress_registry( - config_dir=repo_root / "config", - workflows_dir=repo_root / ".github" / "workflows", - ingress_path=repo_root / ".github" / "workflows" / "docs-aw-ingress.yml", - ) - - def test_client_entrypoints_match_ingress_route_union(self) -> None: - repo_root = pathlib.Path(__file__).parent.parent - for org_key in ("obs", "docs"): - validate_client_entrypoint_permissions( - org_key=org_key, - config_dir=repo_root / "config", - workflows_dir=repo_root / ".github" / "workflows", - template_dir=repo_root / ".github" / "remote-workflow-template", - ) diff --git a/tests/test_org_config.py b/tests/test_org_config.py index 68b86da5..a5cba4d0 100644 --- a/tests/test_org_config.py +++ b/tests/test_org_config.py @@ -6,6 +6,7 @@ import pathlib import sys +import pytest sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "scripts")) @@ -64,6 +65,33 @@ def test_unions_org_files_only_ignores_root_active_repositories( merged = common.merge_active_repositories_from_org_trees(tmp_path) assert merged == ["elastic/from-obs"] + def test_merge_token_policies_detects_cross_org_conflict( + self, tmp_path: pathlib.Path + ) -> None: + for org, policy in ( + ("obs", "token-policy-obs"), + ("docs", "token-policy-docs"), + ): + (tmp_path / org).mkdir() + (tmp_path / org / "workflow-registry.json").write_text( + json.dumps({"workflows": []}), encoding="utf-8" + ) + (tmp_path / org / "active-repositories.json").write_text( + json.dumps( + { + "repositories": [ + { + "repository": "elastic/shared", + "token-policy": policy, + } + ] + } + ), + encoding="utf-8", + ) + with pytest.raises(SystemExit, match="Conflicting token-policy"): + common.merge_repository_token_policies_from_org_trees(tmp_path) + class TestEnabledCompoundIdsFromBody: def test_three_part_and_legacy(self) -> None: diff --git a/tests/test_validate_aw_workflow_prelude.py b/tests/test_validate_aw_workflow_prelude.py index c5f228b5..767a778a 100644 --- a/tests/test_validate_aw_workflow_prelude.py +++ b/tests/test_validate_aw_workflow_prelude.py @@ -5,57 +5,67 @@ import pathlib import sys +import pytest sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "scripts")) import validate_aw_workflow_prelude as validator # noqa: E402 -def test_list_workflow_files_includes_docs_and_oblt_wrappers() -> None: - names = {p.name for p in validator.list_workflow_files()} +def test_list_subject_workflows_includes_route_wrappers() -> None: + names = {p.name for p in validator.list_subject_workflows()} assert "oblt-aw-automerge.yml" in names assert "docs-aw-ai-menu.yml" in names + assert "docs-aw-pr-ai-menu-collect.yml" in names assert "docs-aw-pr-ai-menu.yml" in names - assert "oblt-aw-ingress.yml" not in names - assert "docs-aw-ingress.yml" not in names assert "aw-prelude.yml" not in names + assert "docs-aw-event-issues.yml" not in names assert "trg-oblt-aw-automerge.yml" not in names -def test_validate_aw_wrapper_rejects_prelude_job(tmp_path: pathlib.Path) -> None: +def test_validate_workflow_rejects_missing_shared_proceed( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: workflows = tmp_path / ".github" / "workflows" workflows.mkdir(parents=True) bad = workflows / "docs-aw-test.yml" bad.write_text( - "name: Test\non:\n workflow_call:\njobs:\n" - " prelude:\n uses: ./.github/workflows/aw-prelude.yml\n", + "name: Test\non:\n workflow_call:\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n", encoding="utf-8", ) - errors = validator.validate_aw_wrapper_no_prelude(bad) - assert len(errors) == 2 + monkeypatch.setattr(validator, "WORKFLOWS_DIR", workflows) + errors = validator.validate_workflow(bad) + assert len(errors) == 1 + assert "shared-proceed" in errors[0] -def test_validate_aw_wrapper_accepts_without_prelude(tmp_path: pathlib.Path) -> None: +def test_validate_workflow_rejects_inline_prelude( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch +) -> None: workflows = tmp_path / ".github" / "workflows" workflows.mkdir(parents=True) - good = workflows / "docs-aw-test.yml" - good.write_text( - "name: Test\non:\n workflow_call:\njobs:\n" - " run:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n", + bad = workflows / "docs-aw-test.yml" + bad.write_text( + "name: Test\non:\n workflow_call:\n inputs:\n shared-proceed:\n required: true\n type: string\njobs:\n" + " prelude:\n uses: ./.github/workflows/aw-prelude.yml\n" + " with:\n control-plane-workflows: '[\"docs-aw-test.yml\"]'\n", encoding="utf-8", ) - assert validator.validate_aw_wrapper_no_prelude(good) == [] + monkeypatch.setattr(validator, "WORKFLOWS_DIR", workflows) + errors = validator.validate_workflow(bad) + assert any("prelude job" in err for err in errors) + assert any("must not call aw-prelude.yml" in err for err in errors) -def test_validate_ingress_requires_prelude_and_route_jobs( - tmp_path: pathlib.Path, +def test_validate_workflow_accepts_shared_proceed_route( + tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: workflows = tmp_path / ".github" / "workflows" workflows.mkdir(parents=True) - bad = workflows / "docs-aw-ingress.yml" - bad.write_text( - "name: Ingress\non:\n workflow_call:\njobs:\n prelude:\n uses: ./.github/workflows/aw-prelude.yml\n", + good = workflows / "oblt-aw-test.yml" + good.write_text( + "name: Test\non:\n workflow_call:\n inputs:\n shared-proceed:\n required: true\n type: string\njobs:\n run:\n if: inputs.shared-proceed == 'true'\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n", encoding="utf-8", ) - errors = validator.validate_ingress(bad) - assert any("route-*" in err for err in errors) + monkeypatch.setattr(validator, "WORKFLOWS_DIR", workflows) + assert validator.validate_workflow(good) == [] diff --git a/tests/test_workflow_registry.py b/tests/test_workflow_registry.py index efefbfe8..0deff6b0 100644 --- a/tests/test_workflow_registry.py +++ b/tests/test_workflow_registry.py @@ -31,40 +31,49 @@ def _write_org( ) -class TestBuildControlPlaneWorkflowIndex: - def test_maps_files_to_compound_ids(self, tmp_path: pathlib.Path) -> None: +class TestResolveCompoundId: + def test_single_file_entry(self, tmp_path: pathlib.Path) -> None: _write_org( tmp_path, "obs", [ { "id": "automerge", - "ingress_routes": [{"id": "automerge"}], - }, + "control_plane_workflows": ["oblt-aw-automerge.yml"], + } + ], + ) + assert ( + wr.resolve_compound_id(tmp_path, "oblt-aw-automerge.yml") == "obs:automerge" + ) + + def test_multi_file_entry(self, tmp_path: pathlib.Path) -> None: + _write_org( + tmp_path, + "obs", + [ { "id": "security", - "ingress_routes": [ - {"id": "security-detector"}, - {"id": "security-fixer"}, + "control_plane_workflows": [ + "oblt-aw-security-detector.yml", + "oblt-aw-security-fixer.yml", ], - }, + } ], ) - index = wr.build_control_plane_workflow_index(tmp_path) - assert index["oblt-aw-automerge.yml"].compound_id == "obs:automerge" - assert index["oblt-aw-security-fixer.yml"].compound_id == "obs:security" - + assert ( + wr.resolve_compound_id(tmp_path, "oblt-aw-security-fixer.yml") + == "obs:security" + ) -class TestResolveCompoundId: - def test_resolves_control_plane_workflow(self, tmp_path: pathlib.Path) -> None: + def test_unknown_file_raises(self, tmp_path: pathlib.Path) -> None: _write_org( tmp_path, "obs", - [{"id": "automerge", "ingress_routes": [{"id": "automerge"}]}], - ) - assert ( - wr.resolve_compound_id(tmp_path, "oblt-aw-automerge.yml") == "obs:automerge" + [{"id": "automerge", "control_plane_workflows": ["oblt-aw-automerge.yml"]}], ) + with pytest.raises(ValueError, match="not listed"): + wr.resolve_compound_id(tmp_path, "oblt-aw-missing.yml") class TestValidateRegistryAgainstWorkflows: @@ -74,7 +83,7 @@ def test_flags_missing_registry_entry( _write_org( tmp_path, "obs", - [{"id": "automerge", "ingress_routes": [{"id": "automerge"}]}], + [{"id": "automerge", "control_plane_workflows": ["oblt-aw-automerge.yml"]}], ) workflows = tmp_path / "workflows" workflows.mkdir() diff --git a/tests/unit/classifyAutomergeDependencyCollection.test.ts b/tests/unit/classifyAutomergeDependencyCollection.test.ts index cbb7c8e9..ca786037 100644 --- a/tests/unit/classifyAutomergeDependencyCollection.test.ts +++ b/tests/unit/classifyAutomergeDependencyCollection.test.ts @@ -96,43 +96,6 @@ test('classifyChangedFiles is ambiguous when multiple collections match', () => assert.equal(outcome.collectionIds.length, 2); }); -test('classifyChangedFiles allows active terraform collection', () => { - const collections = [ - ...COLLECTIONS, - { - id: 'terraform', - active: true, - 'file-glob': [ - '**/*.tf', - '**/*.tfvars', - '**/*.tfvars.json', - '**/.terraform.lock.hcl', - '**/terragrunt.hcl', - '**/.opentofu-version', - '**/.terraform-version', - '**/.terragrunt-version', - ], - }, - ]; - const outcome = classifyChangedFiles( - [ - 'environments/dev/main.tf', - 'environments/dev/vars.tfvars', - 'environments/dev/vars.tfvars.json', - 'environments/dev/.terraform.lock.hcl', - 'environments/dev/terragrunt.hcl', - 'environments/dev/.opentofu-version', - 'environments/dev/.terraform-version', - 'environments/dev/.terragrunt-version', - ], - collections - ); - assert.deepEqual(outcome, { - status: 'allowed', - collectionId: 'terraform', - }); -}); - test('buildGateCommentBody includes inactive collection and active list', () => { const body = buildGateCommentBody( { status: 'inactive', collectionId: 'python-dependencies' }, From cb3b271e01cad6f17552c79e60b6f075be49ae50 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:56:47 +0200 Subject: [PATCH 2/3] Add active `terraform` dependency collection for automerge classification (#1049) * Initial plan * feat: add active terraform automerge dependency collection Agent-Logs-Url: https://github.com/elastic/oblt-aw/sessions/b9621ec9-29e4-4f35-a57a-484be55ea4ef Co-authored-by: fr4nc1sc0-r4m0n <215478872+fr4nc1sc0-r4m0n@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fr4nc1sc0-r4m0n <215478872+fr4nc1sc0-r4m0n@users.noreply.github.com> Co-authored-by: Francisco Ramon --- .../obs/automerge-dependency-collections.json | 15 ++++++++ ...ssifyAutomergeDependencyCollection.test.ts | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/config/obs/automerge-dependency-collections.json b/config/obs/automerge-dependency-collections.json index 24082c8e..f12ca136 100644 --- a/config/obs/automerge-dependency-collections.json +++ b/config/obs/automerge-dependency-collections.json @@ -53,6 +53,21 @@ "**/pnpm-lock.yaml" ] }, + { + "id": "terraform", + "description": "Terraform/OpenTofu/Terragrunt dependency and lockfile updates", + "active": true, + "file-glob": [ + "**/*.tf", + "**/*.tfvars", + "**/*.tfvars.json", + "**/.terraform.lock.hcl", + "**/terragrunt.hcl", + "**/.opentofu-version", + "**/.terraform-version", + "**/.terragrunt-version" + ] + }, { "id": "vm-images", "description": "CI runner or container image pin updates", diff --git a/tests/unit/classifyAutomergeDependencyCollection.test.ts b/tests/unit/classifyAutomergeDependencyCollection.test.ts index ca786037..cbb7c8e9 100644 --- a/tests/unit/classifyAutomergeDependencyCollection.test.ts +++ b/tests/unit/classifyAutomergeDependencyCollection.test.ts @@ -96,6 +96,43 @@ test('classifyChangedFiles is ambiguous when multiple collections match', () => assert.equal(outcome.collectionIds.length, 2); }); +test('classifyChangedFiles allows active terraform collection', () => { + const collections = [ + ...COLLECTIONS, + { + id: 'terraform', + active: true, + 'file-glob': [ + '**/*.tf', + '**/*.tfvars', + '**/*.tfvars.json', + '**/.terraform.lock.hcl', + '**/terragrunt.hcl', + '**/.opentofu-version', + '**/.terraform-version', + '**/.terragrunt-version', + ], + }, + ]; + const outcome = classifyChangedFiles( + [ + 'environments/dev/main.tf', + 'environments/dev/vars.tfvars', + 'environments/dev/vars.tfvars.json', + 'environments/dev/.terraform.lock.hcl', + 'environments/dev/terragrunt.hcl', + 'environments/dev/.opentofu-version', + 'environments/dev/.terraform-version', + 'environments/dev/.terragrunt-version', + ], + collections + ); + assert.deepEqual(outcome, { + status: 'allowed', + collectionId: 'terraform', + }); +}); + test('buildGateCommentBody includes inactive collection and active list', () => { const body = buildGateCommentBody( { status: 'inactive', collectionId: 'python-dependencies' }, From 647153cc000e093ee59e42aa7f5b48c058943bd1 Mon Sep 17 00:00:00 2001 From: "fr4nc1sc0.r4m0n" Date: Thu, 4 Jun 2026 13:38:44 +0200 Subject: [PATCH 3/3] chore(obs): set token policy for observability-catalog Assign token-policy-d542feeb05ea so catalog agentic workflows use the correct ephemeral token policy. --- config/obs/active-repositories.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/obs/active-repositories.json b/config/obs/active-repositories.json index 05ade9d4..73814252 100644 --- a/config/obs/active-repositories.json +++ b/config/obs/active-repositories.json @@ -30,7 +30,7 @@ }, { "repository": "elastic/observability-catalog", - "token-policy": "" + "token-policy": "token-policy-d542feeb05ea" }, { "repository": "elastic/observability-github-secrets",