diff --git a/.cursor/rules/protected-oblt-aw-workflow.mdc b/.cursor/rules/protected-oblt-aw-workflow.mdc index b2f8aaac..cc7e92d6 100644 --- a/.cursor/rules/protected-oblt-aw-workflow.mdc +++ b/.cursor/rules/protected-oblt-aw-workflow.mdc @@ -1,14 +1,14 @@ --- -description: Client workflow templates live under remote-workflow-template only +description: Client workflows — `.github/remote-workflow-template/obs/` alwaysApply: true --- # Client workflows — `.github/remote-workflow-template/obs/` -**Do not** add a monolithic `.github/workflows/oblt-aw.yml` entrypoint (removed). +**Do not** add per-workflow `trigger-oblt-aw-*.yml` client entrypoints. -**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. +**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. -**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. +**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. **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 new file mode 100644 index 00000000..1bb635ec --- /dev/null +++ b/.github/remote-workflow-template/docs/.github/workflows/docs-aw.yml @@ -0,0 +1,61 @@ +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 + contents: read + id-token: write + issues: read + pull-requests: read + 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-ai-menu.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-ai-menu.yml deleted file mode 100644 index f63f5311..00000000 --- a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-ai-menu.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Docs Agentic Workflow — AI Issue Menu - -# Distributed to repositories in config/docs/active-repositories.json. - -on: - issues: - types: [opened] - issue_comment: - types: [edited] - 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-ai-menu.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-pr-ai-menu-collect.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu-collect.yml deleted file mode 100644 index ed0d5e40..00000000 --- a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu-collect.yml +++ /dev/null @@ -1,20 +0,0 @@ -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-pr-ai-menu.yml). - -on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - -permissions: - contents: read - issues: read - -jobs: - collect: - permissions: - actions: write - contents: read - issues: read - uses: elastic/oblt-aw/.github/workflows/docs-aw-pr-ai-menu-collect.yml@main # ratchet:exclude diff --git a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu.yml b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu.yml deleted file mode 100644 index c36d1432..00000000 --- a/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu.yml +++ /dev/null @@ -1,35 +0,0 @@ -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] - issue_comment: - types: [edited] - 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: - if: >- - github.event_name != 'workflow_run' || - github.event.workflow_run.conclusion == 'success' - permissions: - actions: read - checks: read - contents: read - issues: write - pull-requests: write - uses: elastic/oblt-aw/.github/workflows/docs-aw-pr-ai-menu.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 new file mode 100644 index 00000000..442435d9 --- /dev/null +++ b/.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw.yml @@ -0,0 +1,83 @@ +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: docs-aw/entrypoint + run: | + gh api \ + --method POST \ + "/repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \ + -f state=success \ + -f target_url="${RUN_URL}" \ + -f description='docs-aw 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 new file mode 100644 index 00000000..d77053ca --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/oblt-aw.yml @@ -0,0 +1,62 @@ +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: write + contents: read + id-token: write + issues: read + pull-requests: read + 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-agent-suggestions.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-agent-suggestions.yml deleted file mode 100644 index 8b955e19..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-agent-suggestions.yml +++ /dev/null @@ -1,18 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-autodoc.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-autodoc.yml deleted file mode 100644 index ee48087a..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-autodoc.yml +++ /dev/null @@ -1,19 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-automerge.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-automerge.yml deleted file mode 100644 index a1a33bc3..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-automerge.yml +++ /dev/null @@ -1,21 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-dependency-review.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-dependency-review.yml deleted file mode 100644 index 37ce25b1..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-dependency-review.yml +++ /dev/null @@ -1,20 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml deleted file mode 100644 index d9920b92..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-duplicate-issue-detector.yml +++ /dev/null @@ -1,19 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml deleted file mode 100644 index e8986680..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-estc-pr-buildkite-detective.yml +++ /dev/null @@ -1,22 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-fixer.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-fixer.yml deleted file mode 100644 index cf57d7bc..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-fixer.yml +++ /dev/null @@ -1,19 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-triage.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-triage.yml deleted file mode 100644 index ac673945..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-issue-triage.yml +++ /dev/null @@ -1,20 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-mention-in-issue.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-mention-in-issue.yml deleted file mode 100644 index 17c93b99..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-mention-in-issue.yml +++ /dev/null @@ -1,20 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml deleted file mode 100644 index 60bb73ed..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml +++ /dev/null @@ -1,18 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml deleted file mode 100644 index 111d3f8e..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml +++ /dev/null @@ -1,19 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml deleted file mode 100644 index 191a09eb..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml +++ /dev/null @@ -1,21 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-detector.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-detector.yml deleted file mode 100644 index 1265a595..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-detector.yml +++ /dev/null @@ -1,19 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-fixer.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-fixer.yml deleted file mode 100644 index d87e14bd..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-fixer.yml +++ /dev/null @@ -1,20 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-triage.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-triage.yml deleted file mode 100644 index b1e90ed3..00000000 --- a/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw-security-triage.yml +++ /dev/null @@ -1,21 +0,0 @@ -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/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml new file mode 100644 index 00000000..1ce5b512 --- /dev/null +++ b/.github/remote-workflow-template/obs/.github/workflows/trigger-oblt-aw.yml @@ -0,0 +1,61 @@ +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: oblt-aw/entrypoint + run: | + gh api \ + --method POST \ + "/repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \ + -f state=success \ + -f target_url="${RUN_URL}" \ + -f description='oblt-aw 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 ee51f47a..978b02dc 100644 --- a/.github/workflows/aw-prelude.yml +++ b/.github/workflows/aw-prelude.yml @@ -1,28 +1,23 @@ name: Agentic Workflow Prelude -# Shared dashboard gating and optional allow-list loading for *-aw-* workflows. -# Call this reusable workflow before agent-specific jobs in oblt-aw-* and docs-aw-* wrappers. +# 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). on: workflow_call: inputs: - control-plane-workflow: - description: >- - Basename of this wrapper under .github/workflows/ (for example - oblt-aw-automerge.yml). Compound org:workflow-id is resolved from - config//workflow-registry.json. - 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: >- - True when dashboard gating allows this workflow (effective-raw empty or - the registry-resolved compound id is listed in enabled-workflows). + description: Always true; route jobs gate on enabled-workflows in ingress value: ${{ jobs.evaluate.outputs.proceed }} effective-raw: description: Raw dashboard read before normalization ('' means all enabled) @@ -44,8 +39,8 @@ on: 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). + Token policy from config//active-repositories.json for this repository; + passed as ingress-token-policy to workflows that call create-token. value: ${{ jobs.evaluate.outputs.token-policy }} permissions: @@ -63,7 +58,10 @@ jobs: contents: read if: >- inputs.load-allowed-authors && - (github.event_name == 'pull_request' || github.event_name == 'issues') + ( + (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' + ) uses: ./.github/workflows/load-allowed-authors.yml evaluate: @@ -78,7 +76,6 @@ jobs: timeout-minutes: 2 outputs: proceed: ${{ steps.gate.outputs.proceed }} - compound-workflow-id: ${{ steps.resolve.outputs.compound-workflow-id }} 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 }} @@ -94,7 +91,6 @@ jobs: fetch-depth: 1 token: ${{ github.token }} sparse-checkout: | - scripts/resolve_control_plane_workflow_id.py scripts/workflow_registry.py scripts/common.py scripts/resolve_repository_token_policy.py @@ -108,27 +104,9 @@ jobs: TARGET_REPOSITORY: ${{ github.repository }} run: python _oblt-aw/scripts/resolve_repository_token_policy.py - - name: Resolve compound workflow id from registry - id: resolve - env: - CONTROL_PLANE_WORKFLOW: ${{ inputs.control-plane-workflow }} - run: python _oblt-aw/scripts/resolve_control_plane_workflow_id.py "${CONTROL_PLANE_WORKFLOW}" --config-dir _oblt-aw/config - - - name: Evaluate dashboard gate + - name: Set proceed output id: gate - env: - EFFECTIVE_RAW: ${{ needs.dashboard.outputs.effective-raw }} - ENABLED_WORKFLOWS: ${{ needs.dashboard.outputs.enabled-workflows }} - ENABLED_WORKFLOW_ID: ${{ steps.resolve.outputs.compound-workflow-id }} - run: | - set -euo pipefail - if [ -z "${EFFECTIVE_RAW}" ]; then - echo "proceed=true" - elif echo "${ENABLED_WORKFLOWS}" | jq -e --arg id "${ENABLED_WORKFLOW_ID}" 'index($id) != null' >/dev/null; then - echo "proceed=true" - else - echo "proceed=false" - fi >> "${GITHUB_OUTPUT}" + run: echo "proceed=true" >> "${GITHUB_OUTPUT}" - name: Pack allow-list outputs id: pack diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4470ade1..bfb55962 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,9 +45,7 @@ jobs: with: python-version: "3.14" cache: pip - cache-dependency-path: | - requirements-ci.txt - requirements-runtime.txt + cache-dependency-path: requirements-ci.txt - name: Install Python test dependencies run: pip install -r requirements-ci.txt @@ -58,6 +56,9 @@ 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 3e2619da..70cd6398 100644 --- a/.github/workflows/docs-aw-ai-menu.yml +++ b/.github/workflows/docs-aw-ai-menu.yml @@ -1,11 +1,26 @@ name: Docs AI menu -# Reusable implementation for the Elastic Docs issue AI menu. Consumer repositories -# install `.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-ai-menu.yml`. +# Reusable implementation for the Elastic Docs issue AI menu. Routed from docs-aw-ingress.yml. # Scripts: `scripts/docs/issue-menu/`. on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from docs-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from docs-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from docs-aw-ingress + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: false @@ -14,27 +29,24 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: docs-aw-ai-menu.yml - post-menu: name: Post or refresh AI menu - needs: prelude if: >- - needs.prelude.outputs.proceed == 'true' && - (github.event_name == 'issues' || github.event_name == 'workflow_dispatch') + ( + 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' runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read issues: write concurrency: - group: docs-ai-menu-${{ github.event.issue.number || github.event.inputs.issue_number }} + group: docs-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).issue.number || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).inputs.issue_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs AI issue menu scripts @@ -58,15 +70,16 @@ jobs: evaluate-trigger: name: Evaluate AI menu trigger - needs: prelude if: >- - needs.prelude.outputs.proceed == 'true' && - github.event_name == 'issue_comment' && - github.event.issue.pull_request == null && + ( + inputs.ingress-event-name != '' && inputs.ingress-event-name || + github.event_name + ) == 'issue_comment' && + fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).issue.pull_request == null && github.actor != 'github-actions[bot]' && - github.event.comment.user.login == 'github-actions[bot]' && - contains(github.event.comment.body, '') && - contains(github.event.comment.body, '') + fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).comment.user.login == 'github-actions[bot]' && + contains(fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).comment.body, '') && + contains(fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).comment.body, '') runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -107,7 +120,7 @@ jobs: contents: read issues: write concurrency: - group: docs-ai-menu-${{ github.event.issue.number || github.event.inputs.issue_number }} + group: docs-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).issue.number || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).inputs.issue_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs AI issue menu scripts @@ -133,19 +146,15 @@ jobs: await fn({github, context, core}) resolve-apm-assets-triage: - needs: [prelude, evaluate-trigger] - if: >- - needs.prelude.outputs.proceed == 'true' && - needs.evaluate-trigger.outputs.triage_triggered == 'true' + needs: [evaluate-trigger] uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: docs-aw-ai-menu.yml run-docs-triage: name: Docs AI / triage - needs: [prelude, evaluate-trigger, resolve-apm-assets-triage] + needs: [evaluate-trigger, resolve-apm-assets-triage] if: >- - needs.prelude.outputs.proceed == 'true' && needs.evaluate-trigger.outputs.triage_triggered == 'true' permissions: actions: read @@ -159,9 +168,8 @@ jobs: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} resolve-apm-assets-issue-scope: - needs: [prelude, evaluate-trigger] + needs: [evaluate-trigger] if: >- - needs.prelude.outputs.proceed == 'true' && needs.evaluate-trigger.outputs.issue_scope_triggered == 'true' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: @@ -169,9 +177,8 @@ jobs: run-docs-issue-scope: name: Docs AI / issue scope - needs: [prelude, evaluate-trigger, resolve-apm-assets-issue-scope] + needs: [evaluate-trigger, resolve-apm-assets-issue-scope] if: >- - needs.prelude.outputs.proceed == 'true' && needs.evaluate-trigger.outputs.issue_scope_triggered == 'true' permissions: actions: read diff --git a/.github/workflows/docs-aw-ingress.yml b/.github/workflows/docs-aw-ingress.yml new file mode 100644 index 00000000..e78a408e --- /dev/null +++ b/.github/workflows/docs-aw-ingress.yml @@ -0,0 +1,187 @@ +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 + 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 + 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 + 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 1976dade..569902b2 100644 --- a/.github/workflows/docs-aw-pr-ai-menu-collect.yml +++ b/.github/workflows/docs-aw-pr-ai-menu-collect.yml @@ -1,28 +1,33 @@ name: Docs PR AI menu collect -# Fork-safe collector for the split-workflow pattern. Consumer repositories install -# `trigger-docs-aw-pr-ai-menu-collect.yml`, which calls this reusable 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. Routed from docs-aw-ingress on pull_request. +# 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 + type: string + default: '' + ingress-event-action: + description: Relayed event action from docs-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from docs-aw-ingress + required: false + type: string + default: '' permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: docs-aw-pr-ai-menu-collect.yml - collect: name: Save PR number artifact - needs: prelude - if: needs.prelude.outputs.proceed == 'true' runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -31,7 +36,7 @@ jobs: steps: - name: Write pull request number env: - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(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 387fc005..cf082b30 100644 --- a/.github/workflows/docs-aw-pr-ai-menu.yml +++ b/.github/workflows/docs-aw-pr-ai-menu.yml @@ -1,11 +1,26 @@ name: Docs PR AI menu -# Reusable implementation for the Elastic Docs PR AI menu. Consumer repositories -# install `.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu.yml`. +# Reusable implementation for the Elastic Docs PR AI menu. Routed from docs-aw-ingress.yml. # Scripts: `scripts/docs/pr-menu/`. on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from docs-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from docs-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from docs-aw-ingress + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: false @@ -14,23 +29,20 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: docs-aw-pr-ai-menu.yml - post-menu: name: Post or refresh AI PR menu - needs: prelude if: >- - needs.prelude.outputs.proceed == 'true' && ( - (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') || - github.event_name == 'workflow_dispatch' - ) + ( + inputs.ingress-event-name != '' && inputs.ingress-event-name || + github.event_name + ) == 'workflow_run' && + fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).workflow_run.conclusion == 'success' + ) || + ( + inputs.ingress-event-name != '' && inputs.ingress-event-name || + github.event_name + ) == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -40,23 +52,28 @@ jobs: issues: write pull-requests: write concurrency: - 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 }} + group: docs-pr-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).workflow_run.id || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request.number || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).issue.number || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).inputs.pull_request_number }} cancel-in-progress: false steps: - name: Download pull request number artifact - if: github.event_name == 'workflow_run' + if: >- + ( + inputs.ingress-event-name != '' && inputs.ingress-event-name || + github.event_name + ) == 'workflow_run' uses: actions/download-artifact@v8 with: name: pr-number - run-id: ${{ github.event.workflow_run.id }} + run-id: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Resolve pull request number id: resolve-pr env: - WORKFLOW_DISPATCH_PR: ${{ github.event.inputs.pull_request_number }} + EVENT_NAME: ${{ inputs.ingress-event-name != '' && inputs.ingress-event-name || github.event_name }} + WORKFLOW_DISPATCH_PR: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).inputs.pull_request_number }} run: | - if [ "${{ github.event_name }}" = "workflow_run" ]; then + if [ "${EVENT_NAME}" = "workflow_run" ]; then echo "number=$(cat pr_number.txt)" >> "$GITHUB_OUTPUT" else echo "number=${WORKFLOW_DISPATCH_PR}" >> "$GITHUB_OUTPUT" @@ -85,15 +102,16 @@ jobs: evaluate-trigger: name: Evaluate AI PR menu trigger - needs: prelude if: >- - needs.prelude.outputs.proceed == 'true' && - github.event_name == 'issue_comment' && - github.event.issue.pull_request != null && + ( + inputs.ingress-event-name != '' && inputs.ingress-event-name || + github.event_name + ) == 'issue_comment' && + fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).issue.pull_request != null && github.actor != 'github-actions[bot]' && - github.event.comment.user.login == 'github-actions[bot]' && - contains(github.event.comment.body, '') && - contains(github.event.comment.body, '') + fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).comment.user.login == 'github-actions[bot]' && + contains(fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).comment.body, '') && + contains(fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).comment.body, '') runs-on: ubuntu-latest timeout-minutes: 10 permissions: @@ -134,7 +152,7 @@ jobs: issues: write pull-requests: write concurrency: - group: docs-pr-ai-menu-${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pull_request_number }} + group: docs-pr-ai-menu-${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request.number || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).issue.number || fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).inputs.pull_request_number }} cancel-in-progress: false steps: - name: Checkout oblt-aw for Docs PR AI menu scripts @@ -157,10 +175,7 @@ jobs: await fn({github, context, core}) resolve-apm-assets: - needs: [prelude, evaluate-trigger] - if: >- - needs.prelude.outputs.proceed == 'true' && - needs.evaluate-trigger.outputs.docs_review_triggered == 'true' + needs: [evaluate-trigger] uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: docs-aw-pr-ai-menu.yml @@ -170,9 +185,8 @@ jobs: run-docs-review: name: Docs AI / docs review - needs: [prelude, evaluate-trigger, resolve-apm-assets] + needs: [evaluate-trigger, resolve-apm-assets] if: >- - needs.prelude.outputs.proceed == 'true' && needs.evaluate-trigger.outputs.docs_review_triggered == 'true' permissions: actions: read diff --git a/.github/workflows/oblt-aw-agent-suggestions.yml b/.github/workflows/oblt-aw-agent-suggestions.yml index cea3c407..e320acc1 100644 --- a/.github/workflows/oblt-aw-agent-suggestions.yml +++ b/.github/workflows/oblt-aw-agent-suggestions.yml @@ -1,6 +1,32 @@ name: Agent Suggestions on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -9,17 +35,7 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-agent-suggestions.yml - resolve-apm-assets: - needs: prelude - if: needs.prelude.outputs.proceed == 'true' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-agent-suggestions.yml @@ -40,12 +56,11 @@ jobs: - `expires`: `24h` agent-suggestions: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: contents: read issues: write pull-requests: read - if: needs.prelude.outputs.proceed == 'true' uses: elastic/ai-github-actions/.github/workflows/gh-aw-agent-suggestions.lock.yml@main with: title-prefix: "[oblt-aw][agent-suggestions]" diff --git a/.github/workflows/oblt-aw-autodoc.yml b/.github/workflows/oblt-aw-autodoc.yml index ad0e88f0..fd3e21d7 100644 --- a/.github/workflows/oblt-aw-autodoc.yml +++ b/.github/workflows/oblt-aw-autodoc.yml @@ -1,6 +1,32 @@ name: Automated Documentation Analysis and Improvement on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -9,18 +35,9 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-autodoc.yml # Step 1: Detect docs drift from recent code changes and create an issue with findings resolve-apm-assets-audit: - needs: prelude - if: needs.prelude.outputs.proceed == 'true' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-autodoc.yml @@ -53,12 +70,11 @@ jobs: **Team notification (mandatory):** Always @mention @elastic/observablt-ci at the top of the issue body so the team receives notifications. Example: "cc @elastic/observablt-ci" or "Notify: @elastic/observablt-ci" at the start of the body. audit: - needs: [prelude, resolve-apm-assets-audit] + needs: [resolve-apm-assets-audit] permissions: contents: read issues: write pull-requests: read - if: needs.prelude.outputs.proceed == 'true' uses: elastic/ai-github-actions/.github/workflows/gh-aw-docs-patrol.lock.yml@main with: lookback-window: 1 day ago diff --git a/.github/workflows/oblt-aw-automerge.yml b/.github/workflows/oblt-aw-automerge.yml index 97cbad1e..eb6028fc 100644 --- a/.github/workflows/oblt-aw-automerge.yml +++ b/.github/workflows/oblt-aw-automerge.yml @@ -1,6 +1,36 @@ name: Automerge on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-token-policy: + description: Token policy resolved by oblt-aw-ingress prelude + required: true + type: string + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: description: "Token for GH-AW mention-in-pr (Copilot); must be supplied by the ingress caller." @@ -10,24 +40,9 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-automerge.yml - load-allowed-authors: true - # Single PR from github.event.pull_request. PR fields only (pull_request trigger). + # Single PR from fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request. PR fields only (pull_request trigger). verify: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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') runs-on: ubuntu-latest permissions: actions: read @@ -66,7 +81,7 @@ jobs: id: validate uses: actions/github-script@v9 env: - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request.number }} with: github-token: ${{ github.token }} script: | @@ -125,7 +140,7 @@ jobs: id: check-collection uses: actions/github-script@v9 env: - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request.number }} with: github-token: ${{ github.token }} script: | @@ -141,7 +156,7 @@ jobs: core.setOutput('collection-id', collectionId || ''); resolve-apm-assets: - needs: [prelude, verify, check-dependency-collection] + needs: [verify, check-dependency-collection] if: >- needs.verify.outputs.proceed == 'true' && needs.check-dependency-collection.outputs.allowed == 'true' @@ -149,10 +164,10 @@ jobs: with: control-plane-workflow: oblt-aw-automerge.yml platform-additional-instructions: | - Target pull request number: ${{ github.event.pull_request.number }}. + Target pull request number: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request.number }}. approve: - needs: [prelude, verify, check-dependency-collection, resolve-apm-assets] + needs: [verify, check-dependency-collection, resolve-apm-assets] if: >- needs.verify.outputs.proceed == 'true' && needs.check-dependency-collection.outputs.allowed == 'true' @@ -164,10 +179,10 @@ jobs: pull-requests: write uses: elastic/ai-github-actions/.github/workflows/gh-aw-mention-in-pr.lock.yml@main with: - allowed-bot-users: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + allowed-bot-users: ${{ inputs.ingress-allowed-pr-authors-csv }} additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} prompt: | - For pull request #${{ github.event.pull_request.number }}: evaluate automerge eligibility. + For pull request #${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(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. @@ -188,7 +203,6 @@ jobs: # and pascalgn/automerge-action config. Author/label rules are enforced in `verify` (validateAutomergePr). automerge: needs: - - prelude - verify - check-dependency-collection - approve @@ -208,7 +222,7 @@ jobs: uses: pascalgn/automerge-action@7961b8b5eec56cc088c140b56d864285eabd3f67 # v0.16.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PULL_REQUEST: ${{ github.event.pull_request.number }} + PULL_REQUEST: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(github.event)).pull_request.number }} MERGE_LABELS: "oblt-aw/ai/merge-ready" UPDATE_LABELS: "oblt-aw/ai/merge-ready" MERGE_METHOD: squash @@ -222,29 +236,23 @@ jobs: # "merge queue" is required. Enabling native "merge when ready" enqueues the PR instead. # Do not pass --delete-branch here: gh rejects it when merge queue is enabled. enable-merge-when-ready: - needs: [automerge, prelude] + needs: [automerge] if: always() && needs.automerge.outputs.merge_result == 'merge_failed' runs-on: ubuntu-latest timeout-minutes: 5 permissions: id-token: write steps: - - name: Create ephemeral GitHub token (configured policy) - id: create-token-explicit - if: ${{ needs.prelude.outputs.token-policy != '' }} + - name: Create ephemeral GitHub token + id: create-token uses: elastic/oblt-actions/github/create-token@v1 with: - token-policy: ${{ needs.prelude.outputs.token-policy }} - - - name: Create ephemeral GitHub token (Vault auto policy) - id: create-token-auto - if: ${{ needs.prelude.outputs.token-policy == '' }} - uses: elastic/oblt-actions/github/create-token@v1 + token-policy: ${{ inputs.ingress-token-policy }} - name: Enable merge when ready (merge queue fallback) env: - GH_TOKEN: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} + GH_TOKEN: ${{ steps.create-token.outputs.token }} + PR_NUMBER: ${{ fromJSON(inputs.ingress-event-payload-json != '' && inputs.ingress-event-payload-json || toJSON(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 7464bb68..cd31c475 100644 --- a/.github/workflows/oblt-aw-dependency-review.yml +++ b/.github/workflows/oblt-aw-dependency-review.yml @@ -1,6 +1,36 @@ name: Dependency Review on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-token-policy: + description: Token policy resolved by oblt-aw-ingress prelude + required: true + type: string + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -9,22 +39,8 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-dependency-review.yml - load-allowed-authors: true resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-dependency-review.yml @@ -58,20 +74,15 @@ jobs: - The comment's "Labels Applied" section must reflect labels you actually applied via add_labels, not merely recommended. If you applied a label, say so; if you did not apply any, say "No labels applied." dependency-review: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: read issues: write pull-requests: write - if: >- - needs.prelude.outputs.proceed == '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) uses: elastic/ai-github-actions/.github/workflows/gh-aw-dependency-review.lock.yml@main with: - allowed-bot-users: ${{ needs.prelude.outputs.allowed-pr-authors-csv }} + allowed-bot-users: ${{ inputs.ingress-allowed-pr-authors-csv }} classification-labels: "oblt-aw/ai/merge-ready" additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} secrets: @@ -82,7 +93,7 @@ jobs: # remove+re-add `oblt-aw/ai/merge-ready` when present so ingress sees a `labeled` event from the # installation token (GITHUB_TOKEN label writes do not trigger downstream workflows). signal-dependency-review-followups: - needs: [dependency-review, prelude] + needs: [dependency-review] runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -90,22 +101,16 @@ jobs: id-token: write pull-requests: write steps: - - name: Create ephemeral GitHub token (configured policy) - id: create-token-explicit - if: ${{ needs.prelude.outputs.token-policy != '' }} + - name: Create ephemeral GitHub token + id: create-token uses: elastic/oblt-actions/github/create-token@v1 with: - token-policy: ${{ needs.prelude.outputs.token-policy }} - - - name: Create ephemeral GitHub token (Vault auto policy) - id: create-token-auto - if: ${{ needs.prelude.outputs.token-policy == '' }} - uses: elastic/oblt-actions/github/create-token@v1 + token-policy: ${{ inputs.ingress-token-policy }} - name: Re-apply merge-ready label to emit installation-token labeled event uses: actions/github-script@v9 with: - github-token: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} + github-token: ${{ steps.create-token.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; diff --git a/.github/workflows/oblt-aw-duplicate-issue-detector.yml b/.github/workflows/oblt-aw-duplicate-issue-detector.yml index 31508356..2d22ba68 100644 --- a/.github/workflows/oblt-aw-duplicate-issue-detector.yml +++ b/.github/workflows/oblt-aw-duplicate-issue-detector.yml @@ -2,6 +2,32 @@ name: Duplicate Issue Detector on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -10,38 +36,17 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-duplicate-issue-detector.yml - resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == 'true' && - ( - (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 duplicate-issue-detector: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: contents: read issues: write pull-requests: read - if: >- - needs.prelude.outputs.proceed == 'true' && - ( - (github.event_name == 'issues' && github.event.action == 'opened') || - github.event_name == 'workflow_dispatch' - ) uses: elastic/ai-github-actions/.github/workflows/gh-aw-duplicate-issue-detector.lock.yml@main with: additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} diff --git a/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml b/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml index 582742b4..76bfbffb 100644 --- a/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml +++ b/.github/workflows/oblt-aw-estc-pr-buildkite-detective.yml @@ -2,6 +2,32 @@ name: PR Buildkite Detective on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -12,21 +38,8 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-estc-pr-buildkite-detective.yml resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == 'true' && - 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 @@ -34,14 +47,13 @@ jobs: If a step fails, check if the failure is reported as a GitHub issue labeled `flaky-test`. Reference the GitHub issue if so. estc-pr-buildkite-detective: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: read issues: read pull-requests: write if: >- - needs.prelude.outputs.proceed == 'true' && github.event_name == 'status' && github.event.state == 'failure' && contains(github.event.context, 'buildkite') diff --git a/.github/workflows/oblt-aw-ingress.yml b/.github/workflows/oblt-aw-ingress.yml new file mode 100644 index 00000000..7ac42fd2 --- /dev/null +++ b/.github/workflows/oblt-aw-ingress.yml @@ -0,0 +1,532 @@ +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 + 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 + 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 + 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-token-policy: ${{ needs.prelude.outputs.token-policy }} + 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 + 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-token-policy: ${{ needs.prelude.outputs.token-policy }} + 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 + 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 + 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 + 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 + 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 + 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-token-policy: ${{ needs.prelude.outputs.token-policy }} + 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 + 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 + 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-token-policy: ${{ needs.prelude.outputs.token-policy }} + 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 + 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 + 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 + 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-token-policy: ${{ needs.prelude.outputs.token-policy }} + 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 + 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 48dffd80..0462ed97 100644 --- a/.github/workflows/oblt-aw-issue-fixer.yml +++ b/.github/workflows/oblt-aw-issue-fixer.yml @@ -1,28 +1,39 @@ name: Issue Fixer on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-issue-fixer.yml resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == 'true' && - 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 @@ -63,7 +74,7 @@ jobs: - Do not perform merge operations in this workflow. run: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: write @@ -71,7 +82,6 @@ jobs: issues: write pull-requests: write if: >- - needs.prelude.outputs.proceed == 'true' && github.event.issue.pull_request == null && startsWith(github.event.comment.body, '/ai implement') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) && diff --git a/.github/workflows/oblt-aw-issue-triage.yml b/.github/workflows/oblt-aw-issue-triage.yml index 613e7eee..f428dcb2 100644 --- a/.github/workflows/oblt-aw-issue-triage.yml +++ b/.github/workflows/oblt-aw-issue-triage.yml @@ -2,6 +2,32 @@ name: Issue Triage on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -10,36 +36,20 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-issue-triage.yml resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == 'true' && - 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 issue-triage: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: read discussions: write issues: write pull-requests: write - if: >- - needs.prelude.outputs.proceed == 'true' && - github.event_name == 'issues' && - github.event.action == 'opened' 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 }} diff --git a/.github/workflows/oblt-aw-mention-in-issue.yml b/.github/workflows/oblt-aw-mention-in-issue.yml index aa9cb826..5f33378f 100644 --- a/.github/workflows/oblt-aw-mention-in-issue.yml +++ b/.github/workflows/oblt-aw-mention-in-issue.yml @@ -2,6 +2,32 @@ name: Mention in Issue on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -10,30 +36,14 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-mention-in-issue.yml resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-mention-in-issue.yml mention-in-issue: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: write @@ -41,7 +51,6 @@ jobs: issues: write pull-requests: write if: >- - needs.prelude.outputs.proceed == 'true' && github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.issue.pull_request == null && 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 cf410062..46bbe0be 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 @@ -1,6 +1,32 @@ name: Resource Not Accessible by Integration Detector on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -9,20 +35,11 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-detector.yml discover: - needs: prelude permissions: actions: read contents: read - if: needs.prelude.outputs.proceed == 'true' && github.event_name == 'schedule' runs-on: ubuntu-latest outputs: workflows: ${{ steps.list.outputs.workflows }} @@ -38,7 +55,7 @@ jobs: GH_TOKEN: ${{ github.token }} resolve-apm-assets: - needs: [prelude, discover] + needs: [discover] if: needs.discover.result == 'success' && needs.discover.outputs.workflows != '[]' uses: ./.github/workflows/aw-resolve-apm-assets.yml with: 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 ca7a250d..17fb90f8 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 @@ -1,28 +1,39 @@ 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 + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-fixer.yml - load-allowed-authors: true resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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') uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-fixer.yml @@ -64,7 +75,7 @@ jobs: - Do not perform merge operations in this workflow. res-not-accessible-integration-fixer: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: write @@ -72,14 +83,13 @@ jobs: issues: write pull-requests: write if: >- - needs.prelude.outputs.proceed == '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') uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-fixer.lock.yml@main with: - allowed-bot-users: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.ingress-allowed-issue-authors-csv }} additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} secrets: inherit 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 9336d05e..d452c54f 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 @@ -1,34 +1,47 @@ 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 + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-token-policy: + description: Token policy resolved by oblt-aw-ingress prelude + required: true + type: string + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + 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: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-triage.yml - load-allowed-authors: true resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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') - ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-resource-not-accessible-by-integration-triage.yml @@ -218,23 +231,16 @@ jobs: - [Troubleshooting Permissions](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token) res-not-accessible-integration-triage: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: read discussions: write issues: write pull-requests: write - if: >- - needs.prelude.outputs.proceed == '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') - ) uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-triage.lock.yml@main with: - allowed-bot-users: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.ingress-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 }} secrets: @@ -244,7 +250,7 @@ jobs: # mint only in this follow-up job; when the fixer preconditions hold, remove+re-add `oblt-aw/ai/fix-ready` # so ingress sees a `labeled` event from the installation token. signal-res-not-accessible-triage-followups: - needs: [res-not-accessible-integration-triage, prelude] + needs: [res-not-accessible-integration-triage] runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -252,22 +258,16 @@ jobs: id-token: write issues: write steps: - - name: Create ephemeral GitHub token (configured policy) - id: create-token-explicit - if: ${{ needs.prelude.outputs.token-policy != '' }} + - name: Create ephemeral GitHub token + id: create-token uses: elastic/oblt-actions/github/create-token@v1 with: - token-policy: ${{ needs.prelude.outputs.token-policy }} - - - name: Create ephemeral GitHub token (Vault auto policy) - id: create-token-auto - if: ${{ needs.prelude.outputs.token-policy == '' }} - uses: elastic/oblt-actions/github/create-token@v1 + token-policy: ${{ inputs.ingress-token-policy }} - name: Re-apply fix-ready label to emit installation-token labeled event uses: actions/github-script@v9 with: - github-token: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} + github-token: ${{ steps.create-token.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; diff --git a/.github/workflows/oblt-aw-security-detector.yml b/.github/workflows/oblt-aw-security-detector.yml index 4092a62e..f6bf08af 100644 --- a/.github/workflows/oblt-aw-security-detector.yml +++ b/.github/workflows/oblt-aw-security-detector.yml @@ -2,24 +2,43 @@ name: Security Detector on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-token-policy: + description: Token policy resolved by oblt-aw-ingress prelude + required: true + type: string + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-security-detector.yml scan: - needs: prelude - if: >- - needs.prelude.outputs.proceed == 'true' && - contains(fromJSON('["schedule","workflow_dispatch"]'), github.event_name) permissions: actions: read contents: read @@ -57,26 +76,17 @@ jobs: count=$(grep -cve '^[[:space:]]*$' findings.txt 2>/dev/null || echo 0) echo "count=$count" >> "$GITHUB_OUTPUT" - - name: Create ephemeral GitHub token (configured policy) - id: create-token-explicit - if: >- - steps.scan.outputs.count != '0' && - needs.prelude.outputs.token-policy != '' + - name: Create ephemeral GitHub token + id: create-token + if: ${{ steps.scan.outputs.count != '0' }} uses: elastic/oblt-actions/github/create-token@v1 with: - token-policy: ${{ needs.prelude.outputs.token-policy }} - - - name: Create ephemeral GitHub token (Vault auto policy) - id: create-token-auto - if: >- - steps.scan.outputs.count != '0' && - needs.prelude.outputs.token-policy == '' - uses: elastic/oblt-actions/github/create-token@v1 + token-policy: ${{ inputs.ingress-token-policy }} - name: Create issues from findings if: ${{ steps.scan.outputs.count != '0' }} env: - GH_TOKEN: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} + GH_TOKEN: ${{ steps.create-token.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 04f00cb6..5bcc6ba2 100644 --- a/.github/workflows/oblt-aw-security-fixer.yml +++ b/.github/workflows/oblt-aw-security-fixer.yml @@ -1,6 +1,32 @@ name: Security Issue Fixer on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -9,29 +35,24 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-security-fixer.yml - load-allowed-authors: true resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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')) - ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-security-fixer.yml - platform-additional-instructions: | + + security-issue-fixer: + needs: [resolve-apm-assets] + permissions: + actions: read + contents: write + discussions: write + issues: write + pull-requests: write + uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-fixer.lock.yml@main + with: + allowed-bot-users: ${{ inputs.ingress-allowed-issue-authors-csv }} + additional-instructions: | Your task is to fix security issues labeled for remediation. This workflow uses agentic workflows from elastic/ai-github-actions. **Execution Preconditions:** @@ -68,27 +89,6 @@ jobs: **Merge Policy:** - Do not merge automatically. - Do not perform merge operations in this workflow. - - security-issue-fixer: - needs: [prelude, resolve-apm-assets] - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - if: >- - needs.prelude.outputs.proceed == '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')) - ) - uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-fixer.lock.yml@main - with: - allowed-bot-users: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} - additional-instructions: ${{ needs.resolve-apm-assets.outputs.resolved-additional-instructions }} secrets: inherit request-reviewers: diff --git a/.github/workflows/oblt-aw-security-triage.yml b/.github/workflows/oblt-aw-security-triage.yml index beef48b4..fa233260 100644 --- a/.github/workflows/oblt-aw-security-triage.yml +++ b/.github/workflows/oblt-aw-security-triage.yml @@ -1,6 +1,36 @@ name: Security Issue Triage on: workflow_call: + inputs: + ingress-event-name: + description: Relayed event name from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-action: + description: Relayed event action from oblt-aw-ingress + required: false + type: string + default: '' + ingress-event-payload-json: + description: Relayed github.event JSON from oblt-aw-ingress + required: false + type: string + default: '' + ingress-token-policy: + description: Token policy resolved by oblt-aw-ingress prelude + required: true + type: string + ingress-allowed-pr-authors-csv: + description: Allowed PR bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' + ingress-allowed-issue-authors-csv: + description: Allowed issue bot logins from oblt-aw-ingress prelude + required: false + type: string + default: '' secrets: COPILOT_GITHUB_TOKEN: required: true @@ -10,24 +40,8 @@ permissions: contents: read jobs: - prelude: - permissions: - contents: read - issues: read - uses: ./.github/workflows/aw-prelude.yml - with: - control-plane-workflow: oblt-aw-security-triage.yml - load-allowed-authors: true resolve-apm-assets: - needs: prelude - if: >- - needs.prelude.outputs.proceed == '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') - ) uses: ./.github/workflows/aw-resolve-apm-assets.yml with: control-plane-workflow: oblt-aw-security-triage.yml @@ -107,23 +121,16 @@ jobs: - [Workflow Permission Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions) security-issue-triage: - needs: [prelude, resolve-apm-assets] + needs: [resolve-apm-assets] permissions: actions: read contents: read discussions: write issues: write pull-requests: write - if: >- - needs.prelude.outputs.proceed == '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') - ) uses: elastic/ai-github-actions/.github/workflows/gh-aw-issue-triage.lock.yml@main with: - allowed-bot-users: ${{ needs.prelude.outputs.allowed-issue-authors-csv }} + allowed-bot-users: ${{ inputs.ingress-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 }} secrets: @@ -133,7 +140,7 @@ jobs: # mint only in this follow-up job; remove+re-add `oblt-aw/ai/fix-ready` when the fixer preconditions # hold so ingress sees a `labeled` event from the installation token. signal-security-triage-followups: - needs: [security-issue-triage, prelude] + needs: [security-issue-triage] runs-on: ubuntu-latest timeout-minutes: 5 permissions: @@ -141,22 +148,16 @@ jobs: id-token: write issues: write steps: - - name: Create ephemeral GitHub token (configured policy) - id: create-token-explicit - if: ${{ needs.prelude.outputs.token-policy != '' }} + - name: Create ephemeral GitHub token + id: create-token uses: elastic/oblt-actions/github/create-token@v1 with: - token-policy: ${{ needs.prelude.outputs.token-policy }} - - - name: Create ephemeral GitHub token (Vault auto policy) - id: create-token-auto - if: ${{ needs.prelude.outputs.token-policy == '' }} - uses: elastic/oblt-actions/github/create-token@v1 + token-policy: ${{ inputs.ingress-token-policy }} - name: Re-apply fix-ready label to emit installation-token labeled event uses: actions/github-script@v9 with: - github-token: ${{ steps.create-token-explicit.outputs.token || steps.create-token-auto.outputs.token }} + github-token: ${{ steps.create-token.outputs.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; diff --git a/AGENTS.md b/AGENTS.md index e1dea6e2..ec002394 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,15 @@ ## Client entrypoint changes -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). +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). -Do not reintroduce a monolithic `oblt-aw.yml` or `oblt-aw-ingress.yml`. +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). ## Control-plane workflow naming - Shared prelude: `.github/workflows/aw-prelude.yml` (no org prefix). -- Observability reusables: `.github/workflows/oblt-aw-.yml` (each must call `aw-prelude` first; enforced by `scripts/validate_aw_workflow_prelude.py` in CI). Workflows that invoke `gh-aw-*` must also call `aw-resolve-apm-assets.yml` per agent job (`scripts/validate_aw_workflow_resolve_apm_assets.py`). -- Docs reusables: `.github/workflows/docs-aw-*.yml` (same prelude requirement). +- 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). - 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 954da96b..c564c60b 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ See [docs/development/contributing.md](docs/development/contributing.md) for ful 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). - 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-ai-menu.yml`, `trigger-docs-aw-pr-ai-menu-collect.yml`, `trigger-docs-aw-pr-ai-menu.yml`) +- Docs templates: [.github/remote-workflow-template/docs/.github/workflows/](.github/remote-workflow-template/docs/.github/workflows/) (`trigger-docs-aw.yml`, `docs-aw.yml`) ## Repository Scope diff --git a/config/docs/workflow-registry.json b/config/docs/workflow-registry.json index bf43aa28..b87d0f23 100644 --- a/config/docs/workflow-registry.json +++ b/config/docs/workflow-registry.json @@ -1,5 +1,4 @@ { - "section_title": "Documentation", "workflows": [ { "id": "docs-issue-ai-menu", @@ -7,8 +6,10 @@ "description": "Adds a documentation issue AI menu.", "maturity": "experimental", "default_enabled": false, - "control_plane_workflows": [ - "docs-aw-ai-menu.yml" + "ingress_routes": [ + { + "id": "ai-menu" + } ] }, { @@ -17,9 +18,13 @@ "description": "Adds a documentation PR AI menu.", "maturity": "experimental", "default_enabled": false, - "control_plane_workflows": [ - "docs-aw-pr-ai-menu-collect.yml", - "docs-aw-pr-ai-menu.yml" + "ingress_routes": [ + { + "id": "pr-ai-menu-collect" + }, + { + "id": "pr-ai-menu" + } ] } ] diff --git a/config/obs/workflow-registry.json b/config/obs/workflow-registry.json index 2e2d7bbb..8083fca1 100644 --- a/config/obs/workflow-registry.json +++ b/config/obs/workflow-registry.json @@ -1,5 +1,4 @@ { - "section_title": "Observability", "workflows": [ { "id": "agent-suggestions", @@ -7,7 +6,11 @@ "description": "Suggests agentic workflows and improvements for the repository based on analysis of current setup.", "maturity": "early-adoption", "default_enabled": true, - "control_plane_workflows": ["oblt-aw-agent-suggestions.yml"] + "ingress_routes": [ + { + "id": "agent-suggestions" + } + ] }, { "id": "autodoc", @@ -15,7 +18,11 @@ "description": "Analyzes documentation for gaps, outdated content, and inconsistencies; creates issues and PRs to improve docs.", "maturity": "early-adoption", "default_enabled": true, - "control_plane_workflows": ["oblt-aw-autodoc.yml"] + "ingress_routes": [ + { + "id": "autodoc" + } + ] }, { "id": "automerge", @@ -23,7 +30,12 @@ "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, - "control_plane_workflows": ["oblt-aw-automerge.yml"] + "ingress_routes": [ + { + "id": "automerge", + "allowed_bot_users_from": "allowed-pr" + } + ] }, { "id": "dependency-review", @@ -31,7 +43,12 @@ "description": "Analyzes dependency-update PRs from bots, applies merge-ready labels when criteria are met.", "maturity": "early-adoption", "default_enabled": true, - "control_plane_workflows": ["oblt-aw-dependency-review.yml"] + "ingress_routes": [ + { + "id": "dependency-review", + "allowed_bot_users_from": "allowed-pr" + } + ] }, { "id": "duplicate-issue-detector", @@ -39,7 +56,11 @@ "description": "Detects potential duplicate issues when a new issue is opened or when the entrypoint is run manually.", "maturity": "experimental", "default_enabled": false, - "control_plane_workflows": ["oblt-aw-duplicate-issue-detector.yml"] + "ingress_routes": [ + { + "id": "duplicate-issue-detector" + } + ] }, { "id": "issue-triage", @@ -47,7 +68,11 @@ "description": "Triages newly opened issues using the generic issue-triage agentic workflow.", "maturity": "experimental", "default_enabled": false, - "control_plane_workflows": ["oblt-aw-issue-triage.yml"] + "ingress_routes": [ + { + "id": "issue-triage" + } + ] }, { "id": "issue-fixer", @@ -55,7 +80,11 @@ "description": "Executes generic issue fixes requested with /ai implement comments, excluding specialized security and resource-not-accessible flows.", "maturity": "experimental", "default_enabled": false, - "control_plane_workflows": ["oblt-aw-issue-fixer.yml"] + "ingress_routes": [ + { + "id": "issue-fixer" + } + ] }, { "id": "mention-in-issue", @@ -63,7 +92,11 @@ "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, - "control_plane_workflows": ["oblt-aw-mention-in-issue.yml"] + "ingress_routes": [ + { + "id": "mention-in-issue" + } + ] }, { "id": "security", @@ -71,10 +104,18 @@ "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, - "control_plane_workflows": [ - "oblt-aw-security-detector.yml", - "oblt-aw-security-fixer.yml", - "oblt-aw-security-triage.yml" + "ingress_routes": [ + { + "id": "security-detector" + }, + { + "id": "security-fixer", + "allowed_bot_users_from": "allowed-issue" + }, + { + "id": "security-triage", + "allowed_bot_users_from": "allowed-issue" + } ] }, { @@ -83,10 +124,18 @@ "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, - "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" + "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" + } ] }, { @@ -95,7 +144,11 @@ "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, - "control_plane_workflows": ["oblt-aw-estc-pr-buildkite-detective.yml"] + "ingress_routes": [ + { + "id": "estc-pr-buildkite-detective" + } + ] } ] } diff --git a/docs/onboarding/adopting-agentic-workflows.md b/docs/onboarding/adopting-agentic-workflows.md index a2d4796e..26584b59 100644 --- a/docs/onboarding/adopting-agentic-workflows.md +++ b/docs/onboarding/adopting-agentic-workflows.md @@ -24,7 +24,7 @@ Each **organization** owns `config//` (for example `config/obs/`): [`wo ### 2. Add prelude and route conditions -- First job: `uses: ./.github/workflows/aw-prelude.yml` with `control-plane-workflow: .yml` (must appear under that workflow’s `control_plane_workflows` in `workflow-registry.json`) and `load-allowed-authors: true` when PR/issue allow lists apply. +- 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)). ### 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 `control_plane_workflows` (basenames of every `oblt-aw-*` / `docs-aw-*` wrapper that share this dashboard id) under `config//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`. ### 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 basename is listed under the correct `control_plane_workflows` entry in `workflow-registry.json` and prelude passes `control-plane-workflow: `. +- **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. ## References diff --git a/docs/operations/control-plane-dashboard-format.md b/docs/operations/control-plane-dashboard-format.md index 5abe400b..057175a0 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 `control_plane_workflows` (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 `ingress_routes` (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/workflows/README.md b/docs/workflows/README.md index 725ca9a1..c1e5a0c0 100644 --- a/docs/workflows/README.md +++ b/docs/workflows/README.md @@ -11,7 +11,8 @@ 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` under remote-workflow-template): [docs/workflows/docs-aw-client-template.md](docs-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 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 0cc23956..28ea047b 100644 --- a/docs/workflows/aw-prelude.md +++ b/docs/workflows/aw-prelude.md @@ -4,11 +4,9 @@ Source file: [.github/workflows/aw-prelude.yml](../../.github/workflows/aw-prelude.yml) -Shared reusable prelude for agentic workflows (dashboard gating and optional allow lists). +Shared reusable prelude for agentic workflows (dashboard read, optional allow lists, token policy). -Every control-plane `*-aw-*` wrapper (`oblt-aw-*`, `docs-aw-*`) invokes this prelude as its first job before running agent-specific steps. CI enforces this 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. +`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). ## Contract @@ -16,29 +14,24 @@ APM asset resolution (`apm install`, `apm.yml` merge) is **not** part of the pre | Input | Type | Default | Purpose | |-------|------|---------|---------| -| `control-plane-workflow` | string | (required) | Basename of the calling wrapper (for example `oblt-aw-automerge.yml`). Prelude resolves `org:workflow-id` from that org’s [`workflow-registry.json`](../../config/obs/workflow-registry.json) `control_plane_workflows` list. | | `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` | `true` when dashboard gating allows the workflow | +| `proceed` | Always `true` (dashboard gating is enforced per route in ingress) | | `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) | - -### Gating rule - -Same as ingress historically used: - -- `effective-raw` empty → `proceed=true` -- Otherwise `proceed=true` only when the registry-resolved compound id is in `enabled-workflows` +| `token-policy` | Repository token policy from `active-repositories.json` when configured | ## 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-client-template.md](oblt-aw-client-template.md) +- [oblt-aw-ingress.md](oblt-aw-ingress.md) +- [docs-aw-ingress.md](docs-aw-ingress.md) diff --git a/docs/workflows/docs-aw-ai-menu.md b/docs/workflows/docs-aw-ai-menu.md index 4797f65b..e971f06d 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. The client template `trigger-docs-aw-ai-menu.yml` calls this workflow on supported issue events. +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. ## Prerequisites -- Triggered via `workflow_call` from [trigger-docs-aw-ai-menu.yml](../../.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-ai-menu.yml) (distributed client template). +- Triggered via `workflow_call` from [docs-aw-ingress.yml](../../.github/workflows/docs-aw-ingress.yml) (`route-ai-menu`). - 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 fe99a7c0..1a6c9568 100644 --- a/docs/workflows/docs-aw-client-template.md +++ b/docs/workflows/docs-aw-client-template.md @@ -1,69 +1,59 @@ -# Workflow: Client templates `trigger-docs-aw-*.yml` +# Workflow: Client templates `trigger-docs-aw.yml` and `docs-aw.yml` ## Overview -**Source of truth (edit here only):** [.github/remote-workflow-template/docs/.github/workflows/](../../.github/remote-workflow-template/docs/.github/workflows/) +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). -`distribute-client-workflow` installs these files into consumer repositories for every repository listed under [config/docs/active-repositories.json](../../config/docs/active-repositories.json). +Flow: **`trigger-docs-aw.yml`** (events) → **`docs-aw.yml`** (`workflow_dispatch`) → **`docs-aw-ingress.yml`** (routing). -## Split-trigger model - -Each Docs menu has its own client template with **only** the GitHub events that can trigger that workflow, then calls the matching reusable workflow in `elastic/oblt-aw`: +Control-plane reusables are referenced from `elastic/oblt-aw`: ```yaml -uses: elastic/oblt-aw/.github/workflows/docs-aw-.yml@main +uses: elastic/oblt-aw/.github/workflows/docs-aw-ingress.yml@main ``` -Shared dashboard gating runs inside each `docs-aw-*` reusable workflow via [aw-prelude.yml](aw-prelude.md) (first job), not in the client file. - -### Template index - -| Client template | Triggers | Reusable workflow | -|-----------------|----------|-------------------| -| `trigger-docs-aw-ai-menu.yml` | `issues` opened; `issue_comment` edited; `workflow_dispatch` (`issue_number` required) | `docs-aw-ai-menu.yml` | -| `trigger-docs-aw-pr-ai-menu-collect.yml` | `pull_request` (opened, reopened, synchronize, ready_for_review) | `docs-aw-pr-ai-menu-collect.yml` | -| `trigger-docs-aw-pr-ai-menu.yml` | `workflow_run` on the collect workflow (completed, success only); `issue_comment` edited; `workflow_dispatch` (`pull_request_number` required) | `docs-aw-pr-ai-menu.yml` | - -Route-specific conditions (for example PR vs non-PR issue comments, menu checkbox transitions) are enforced inside the `docs-aw-*` reusable workflows after [aw-prelude](aw-prelude.md) runs. +Shared dashboard gating and prelude run in ingress, not in the client trigger file. -### Fork PRs and SEC-043 (split-workflow) +## Installed client workflows -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: +| 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` | -1. **`trigger-docs-aw-pr-ai-menu-collect.yml`** — `pull_request` only; read-only job uploads a `pr-number` artifact (implementation: [docs-aw-pr-ai-menu-collect.yml](../../.github/workflows/docs-aw-pr-ai-menu-collect.yml)). -2. **`trigger-docs-aw-pr-ai-menu.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. +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. -Menu checkbox handling (`issue_comment`) and manual refresh (`workflow_dispatch`) stay on `trigger-docs-aw-pr-ai-menu.yml`. Fork checkbox triggers require org membership (enforced in `scripts/docs/pr-menu/evaluate-trigger.js`). +## Split PR menu pattern -## Configuration +The PR AI menu uses a fork-safe collect leg and a privileged post leg within one trigger: -Top-level permissions on every client template: +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. -- `contents: read` +Menu checkbox handling (`issue_comment`) and manual refresh (`workflow_dispatch`) use the same unified trigger. -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. +On `pull_request` and on the privileged `workflow_run` leg (`workflow_run.event == pull_request`), the trigger posts commit status context `docs-aw/entrypoint` 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. -Job-level permissions on `run-aw` must be at least as permissive as the union of all job scopes in the called `docs-aw-*` reusable (see per-template table below). +## Permissions -| Template | `run-aw` job permissions (union of callee jobs) | -|----------|-------------------------------------------------| -| `trigger-docs-aw-ai-menu.yml` | `actions: read`, `contents: read`, `discussions: write`, `issues: write`, `pull-requests: write` | -| `trigger-docs-aw-pr-ai-menu-collect.yml` | `actions: write`, `contents: read` | -| `trigger-docs-aw-pr-ai-menu.yml` | `actions: read`, `checks: read`, `contents: read`, `issues: write`, `pull-requests: write` | +Control-plane `docs-aw-*` workflows declare permissions on **each job** (workflow root is `contents: read` only). -Required secret mapping (both templates): +| 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`, `contents: read`, `id-token: write`, `issues: read`, `pull-requests: read` (ingress job; routed workflows may need more) | -- `COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}` +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. -## Migration from monolithic `docs-aw.yml` +## Migration from per-workflow `trigger-docs-aw-*.yml` -1. Merge distribution PRs that add `trigger-docs-aw-ai-menu.yml`, `trigger-docs-aw-pr-ai-menu-collect.yml`, and `trigger-docs-aw-pr-ai-menu.yml`. -2. Delete `.github/workflows/docs-aw.yml` in the consumer repository. Remove legacy `docs-aw-ai-menu.yml` / `docs-aw-pr-ai-menu.yml` client files if present; distribution removes paths no longer in the template tree. -3. Update Backstage `workflow_ref` / token policies to reference each installed **`trigger-docs-aw-*.yml`** client workflow file. +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`**. ## 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) diff --git a/docs/workflows/docs-aw-ingress.md b/docs/workflows/docs-aw-ingress.md new file mode 100644 index 00000000..92bad753 --- /dev/null +++ b/docs/workflows/docs-aw-ingress.md @@ -0,0 +1,33 @@ +# 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. + +## 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 eefe1a7a..e7ee35cf 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. The client template `trigger-docs-aw-pr-ai-menu.yml` calls this workflow on supported PR events. +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. ## Prerequisites -- Triggered via `workflow_call` from [trigger-docs-aw-pr-ai-menu.yml](../../.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu.yml) (distributed client template). +- Triggered via `workflow_call` from [docs-aw-ingress.yml](../../.github/workflows/docs-aw-ingress.yml) (`route-pr-ai-menu`). - 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` of [trigger-docs-aw-pr-ai-menu-collect.yml](../../.github/remote-workflow-template/docs/.github/workflows/trigger-docs-aw-pr-ai-menu-collect.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`. +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`. 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 8b64a54b..a70aae8c 100644 --- a/docs/workflows/oblt-aw-client-template.md +++ b/docs/workflows/oblt-aw-client-template.md @@ -1,20 +1,17 @@ -# Workflow: Client templates `trigger-oblt-aw-*.yml` +# Workflow: Client templates `trigger-oblt-aw.yml` and `oblt-aw.yml` ## Overview **Source of truth (edit here only):** [.github/remote-workflow-template/obs/.github/workflows/](../../.github/remote-workflow-template/obs/.github/workflows/) -## Split-trigger model +Consumer repositories install **two** client workflows: -Each agentic workflow has its own client template under `trigger-oblt-aw-.yml` (or a descriptive suffix for multi-step features such as `trigger-oblt-aw-security-triage.yml`). Each file declares **only** the GitHub events that can trigger that workflow, then calls the matching reusable workflow in `elastic/oblt-aw`: +| 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 | -```yaml -uses: elastic/oblt-aw/.github/workflows/oblt-aw-.yml@main -``` - -That removes the large number of skipped ingress jobs on unrelated events (for example issue comments no longer run automerge, dependency-review, and security jobs). - -Shared dashboard gating and allow-list loading run inside each `oblt-aw-*` workflow via [aw-prelude.yml](aw-prelude.md) (first job), not in the client file. +Split per-workflow `trigger-oblt-aw-*.yml` files are **not** distributed anymore. ### Architecture @@ -22,96 +19,83 @@ Shared dashboard gating and allow-list loading run inside each `oblt-aw-*` workf flowchart TB subgraph Consumer["Consumer .github/workflows/"] EVT["GitHub event"] - C_AUTO["trigger-oblt-aw-automerge.yml\non: pull_request"] - C_TRI["trigger-oblt-aw-issue-triage.yml\non: issues"] - C_OTHER["trigger-oblt-aw-*.yml\nother narrow on:"] - DASH["Issue: [oblt-aw] Control Plane Dashboard"] - EVT --> C_AUTO - EVT --> C_TRI - EVT -.->|on: no match| C_OTHER + TRG["trigger-oblt-aw.yml\nall supported on:"] + AW["oblt-aw.yml\non: workflow_dispatch"] + EVT --> TRG + TRG -->|"workflow-dispatch action + event payload"| AW end subgraph OBLT["elastic/oblt-aw"] - R["oblt-aw-* reusable"] - PRE["aw-prelude"] - GET["get-enabled-workflows"] - AG["Agent steps"] - LOCK["Upstream gh-aw lock"] - R --> PRE --> GET - PRE --> AG --> LOCK + 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"] end - - DASH -.->|checkboxes| GET - C_AUTO -->|uses: oblt-aw-automerge@main| R - C_TRI -->|uses: oblt-aw-issue-triage@main| R ``` -Full platform view (distribution, dashboard sync, before/after ingress): [architecture overview — split-trigger diagrams](../architecture/overview.md#split-trigger-vs-monolithic-ingress). - -### Template index - -| Client template | Triggers | Reusable workflow | -|-----------------|----------|-------------------| -| `trigger-oblt-aw-agent-suggestions.yml` | `schedule` | `oblt-aw-agent-suggestions.yml` | -| `trigger-oblt-aw-autodoc.yml` | `schedule` | `oblt-aw-autodoc.yml` | -| `trigger-oblt-aw-automerge.yml` | `pull_request` (opened, synchronize, reopened, labeled) | `oblt-aw-automerge.yml` | -| `trigger-oblt-aw-dependency-review.yml` | `pull_request` (opened, synchronize, reopened) | `oblt-aw-dependency-review.yml` | -| `trigger-oblt-aw-duplicate-issue-detector.yml` | `issues` opened, `workflow_dispatch` | `oblt-aw-duplicate-issue-detector.yml` | -| `trigger-oblt-aw-issue-triage.yml` | `issues` opened | `oblt-aw-issue-triage.yml` | -| `trigger-oblt-aw-issue-fixer.yml` | `issue_comment` created | `oblt-aw-issue-fixer.yml` | -| `trigger-oblt-aw-mention-in-issue.yml` | `issue_comment` created | `oblt-aw-mention-in-issue.yml` | -| `trigger-oblt-aw-security-detector.yml` | `schedule`, `workflow_dispatch` | `oblt-aw-security-detector.yml` | -| `trigger-oblt-aw-security-triage.yml` | `issues` opened, labeled | `oblt-aw-security-triage.yml` | -| `trigger-oblt-aw-security-fixer.yml` | `issues` labeled | `oblt-aw-security-fixer.yml` | -| `trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml` | `schedule` | `oblt-aw-resource-not-accessible-by-integration-detector.yml` | -| `trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml` | `issues` opened, labeled | `oblt-aw-resource-not-accessible-by-integration-triage.yml` | -| `trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml` | `issues` labeled | `oblt-aw-resource-not-accessible-by-integration-fixer.yml` | -| `trigger-oblt-aw-estc-pr-buildkite-detective.yml` | `status` (Buildkite failure only, job `if`) | `oblt-aw-estc-pr-buildkite-detective.yml` | - -Route-specific conditions (labels, `/ai` comment prefix, allow-listed PR authors, and so on) are enforced inside the `oblt-aw-*` reusable workflow after [aw-prelude](aw-prelude.md) runs. - -## Configuration - -Top-level permissions on every client template: - -- `contents: read` - -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. - -Job-level permissions on `run-aw` must be at least as permissive as the union of all job scopes in the called `oblt-aw-*` reusable (see per-template table below). - -| Client template | `run-aw` job permissions (union of callee jobs) | -|-----------------|-----------------------------------------------| -| `trigger-oblt-aw-agent-suggestions.yml` | `contents: read`, `issues: write`, `pull-requests: read` | -| `trigger-oblt-aw-autodoc.yml` | `actions: read`, `contents: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-automerge.yml` | `actions: read`, `contents: write`, `discussions: write`, `id-token: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-dependency-review.yml` | `actions: read`, `contents: read`, `id-token: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-duplicate-issue-detector.yml` | `contents: read`, `issues: write`, `pull-requests: read` | -| `trigger-oblt-aw-estc-pr-buildkite-detective.yml` | `actions: read`, `contents: read`, `issues: read`, `pull-requests: write` | -| `trigger-oblt-aw-issue-fixer.yml` | `actions: read`, `contents: write`, `discussions: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-issue-triage.yml` | `actions: read`, `contents: read`, `discussions: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-mention-in-issue.yml` | `actions: read`, `contents: write`, `discussions: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-resource-not-accessible-by-integration-detector.yml` | `actions: read`, `contents: read`, `issues: write` | -| `trigger-oblt-aw-resource-not-accessible-by-integration-fixer.yml` | `actions: read`, `contents: write`, `discussions: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-resource-not-accessible-by-integration-triage.yml` | `actions: read`, `contents: read`, `discussions: write`, `id-token: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-security-detector.yml` | `actions: read`, `contents: read`, `id-token: write`, `issues: read`, `pull-requests: read` | -| `trigger-oblt-aw-security-fixer.yml` | `actions: read`, `contents: write`, `discussions: write`, `issues: write`, `pull-requests: write` | -| `trigger-oblt-aw-security-triage.yml` | `actions: read`, `contents: read`, `discussions: write`, `id-token: write`, `issues: write`, `pull-requests: write` | +### Trigger events (`trigger-oblt-aw.yml`) + +| 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 | + +### Context relayed to `oblt-aw.yml` + +| 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` | + +### Permissions + +**`trigger-oblt-aw.yml`** + +| 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 | + +The trigger uses `secrets.GITHUB_TOKEN` only (no `create-token`); same-repo dispatch and commit statuses do not need Backstage OIDC. + +The dispatch step does **not** wait for `oblt-aw.yml` to finish. On `pull_request` events, a follow-up step posts commit status context `oblt-aw/entrypoint` 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. + +**`oblt-aw.yml`** + +| Scope | Job | Why | +|-------|-----|-----| +| `contents: read` | workflow root | Default | +| `actions: write` | `ingress` | Ingress may dispatch nested workflows | +| `id-token: write` | `ingress` | Ephemeral tokens in routed workflows | +| `issues: read` | `ingress` | Dashboard gating | +| `pull-requests: read` | `ingress` | Route planning for PR events | ### Secrets | Secret | Templates | |--------|-----------| -| `COPILOT_GITHUB_TOKEN` | All except `trigger-oblt-aw-issue-fixer.yml` and resource fixer (use `secrets: inherit` where noted in template) | -| `BUILDKITE_LOGS_API_TOKEN` → `BUILDKITE_API_TOKEN` | `trigger-oblt-aw-estc-pr-buildkite-detective.yml` only | +| `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) | -## Migration from monolithic entrypoint +## Migration from split triggers -1. Merge distribution PRs that add `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 (one policy per workflow if your org requires narrow OIDC claims). +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`**. ## 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 new file mode 100644 index 00000000..fbaa838e --- /dev/null +++ b/docs/workflows/oblt-aw-ingress.md @@ -0,0 +1,34 @@ +# 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 outputs (`ingress-token-policy`, 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. + +## 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 c5da3ba1..a144d99e 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`, `control_plane_workflows`, optional `section_title`) +- Per-org `config//workflow-registry.json` — workflow metadata (`id`, `name`, `description`, `maturity`, `default_enabled`, `ingress_routes`, 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/oblt_aw_route_specs.py b/scripts/oblt_aw_route_specs.py new file mode 100644 index 00000000..b3ac2901 --- /dev/null +++ b/scripts/oblt_aw_route_specs.py @@ -0,0 +1,279 @@ +#!/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) + + +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))) + ) + + +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_control_plane_workflow_id.py b/scripts/resolve_control_plane_workflow_id.py deleted file mode 100644 index 31e68014..00000000 --- a/scripts/resolve_control_plane_workflow_id.py +++ /dev/null @@ -1,65 +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. - -""" -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/validate_aw_workflow_prelude.py b/scripts/validate_aw_workflow_prelude.py index 7ec73ec8..f7371ddf 100644 --- a/scripts/validate_aw_workflow_prelude.py +++ b/scripts/validate_aw_workflow_prelude.py @@ -15,11 +15,10 @@ # under the License. """ -Validate that every local *-aw-* workflow under .github/workflows/ calls aw-prelude.yml -and is registered in config//workflow-registry.json. +Validate aw-prelude placement for control-plane workflows. -Excludes aw-prelude.yml itself (the shared prelude implementation) and distributed -trg-* and trigger-* client entrypoints, which call elastic/oblt-aw reusable workflows remotely. +- *-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. """ from __future__ import annotations @@ -38,9 +37,11 @@ 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) -def list_subject_workflows() -> list[pathlib.Path]: +def list_workflow_files() -> 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")) @@ -49,37 +50,74 @@ def list_subject_workflows() -> 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 p.name.startswith(("trg-", "trigger-")) ] -def validate_workflow(path: pathlib.Path) -> list[str]: +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]: + 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)" + ) + if PRELUDE_USES.search(text): + errors.append( + f"{path}: must not call aw-prelude.yml (prelude and route gating run in ingress)" + ) + 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' via a prelude job" - ) + 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 main() -> int: - errors: list[str] = [] - subjects = list_subject_workflows() - if not subjects: + paths = list_workflow_files() + if not paths: print("No *-aw-* workflows found to validate.", file=sys.stderr) return 1 - for path in subjects: - errors.extend(validate_workflow(path)) + 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, - {path.name for path in subjects}, + registry_subjects, ) ) @@ -90,8 +128,8 @@ def main() -> int: return 1 print( - f"Validated {len(subjects)} *-aw-* workflow(s) call aw-prelude.yml " - "and match workflow-registry.json." + f"Validated {len(wrappers)} *-aw wrapper(s) and " + f"{len(INGRESS_FILES)} ingress workflow(s)." ) return 0 diff --git a/scripts/validate_ingress_registry.py b/scripts/validate_ingress_registry.py new file mode 100644 index 00000000..f584eb9f --- /dev/null +++ b/scripts/validate_ingress_registry.py @@ -0,0 +1,50 @@ +#!/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_docs_ingress_registry, + validate_obs_ingress_registry, +) + +CONFIG_DIR = Path("config") +WORKFLOWS_DIR = Path(".github/workflows") + + +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", + ) + 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 82e9770b..6e5c1b08 100644 --- a/scripts/workflow_registry.py +++ b/scripts/workflow_registry.py @@ -18,7 +18,7 @@ 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/`` via ``control_plane_workflows``. +workflow files under ``.github/workflows/``, derived from ``ingress_routes``. """ from __future__ import annotations @@ -29,9 +29,10 @@ 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$") -PRELUDE_CONTROL_PLANE_WORKFLOW = re.compile( +LEGACY_CONTROL_PLANE_WORKFLOW = re.compile( r"control-plane-workflow:\s*([^\s#]+)", re.MULTILINE, ) @@ -79,18 +80,23 @@ def parse_registry_entries(org_dir: Path) -> list[RegistryWorkflowEntry]: f"{org_dir}: workflows[{index}].id must be a non-empty string" ) - 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" + context = f"{org_dir}: workflows[{index}]" + try: + route_specs = parse_workflow_ingress_routes( + item, + org_key=org_key, + context=context, ) + except RegistryParseError as exc: + raise ValueError(str(exc)) from exc + normalized: list[str] = [] - for file_index, name in enumerate(files): - if not isinstance(name, str) or not CONTROL_PLANE_WORKFLOW_NAME.match(name): + for route_spec in route_specs: + name = route_spec.workflow_file + if not CONTROL_PLANE_WORKFLOW_NAME.match(name): raise ValueError( - f"{org_dir}: workflows[{index}].control_plane_workflows[{file_index}] " - f"must match *-aw-*.yml, got {name!r}" + f"{org_dir}: workflows[{index}] ingress route " + f"{route_spec.route_id!r} must resolve to *-aw-*.yml, got {name!r}" ) normalized.append(name) entries.append( @@ -114,7 +120,7 @@ def build_control_plane_workflow_index( if filename in index: previous = index[filename] raise ValueError( - f"control_plane_workflows[{filename!r}] is listed under both " + f"ingress route workflow {filename!r} is listed under both " f"{previous.org_key}:{previous.workflow_id} and " f"{entry.org_key}:{entry.workflow_id}" ) @@ -122,18 +128,6 @@ 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, @@ -152,7 +146,7 @@ def validate_registry_against_workflows( for name in sorted(missing): errors.append( f"{workflows_dir / name}: not listed in any " - "workflow-registry.json control_plane_workflows" + "workflow-registry.json ingress_routes" ) stale = registered - subject_workflow_names @@ -170,20 +164,18 @@ def validate_registry_against_workflows( text = path.read_text(encoding="utf-8") if LEGACY_ENABLED_WORKFLOW_ID.search(text): errors.append( - f"{path}: use control-plane-workflow instead of enabled-workflow-id " - "(compound id is resolved from workflow-registry.json)" - ) - continue - match = PRELUDE_CONTROL_PLANE_WORKFLOW.search(text) - if not match: - errors.append( - f"{path}: prelude must pass control-plane-workflow matching this file" + f"{path}: remove enabled-workflow-id " + "(dashboard gating is enforced in ingress route jobs)" ) - continue - declared = match.group(1) - if declared != path_name: + # 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, + ): errors.append( - f"{path}: control-plane-workflow is {declared!r}, expected {path_name!r}" + f"{path}: remove control-plane-workflow from aw-prelude " + "(prelude runs in ingress without per-wrapper gating)" ) - continue return errors diff --git a/tests/test_build_target_operations.py b/tests/test_build_target_operations.py index d287fec9..12cfa209 100644 --- a/tests/test_build_target_operations.py +++ b/tests/test_build_target_operations.py @@ -205,7 +205,8 @@ def _setup_env( / "workflows" ) tmpl.mkdir(parents=True, exist_ok=True) - (tmpl / "trigger-oblt-aw-automerge.yml").write_text("name: client\n") + (tmpl / "trigger-oblt-aw.yml").write_text("name: client\n") + (tmpl / "oblt-aw.yml").write_text("name: entrypoint\n") return output_file def test_no_changes_skips_work( @@ -242,7 +243,8 @@ 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-automerge.yml" in dsts + assert ".github/workflows/trigger-oblt-aw.yml" in dsts + assert ".github/workflows/oblt-aw.yml" in dsts assert "remove_files" in t assert t["remove_files"] == [] @@ -332,7 +334,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-automerge.yml" in dsts + assert ".github/workflows/trigger-oblt-aw.yml" in dsts def test_install_includes_remove_files_for_dropped_templates( self, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path @@ -377,6 +379,9 @@ 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/oblt-aw.yml" in install["remove_files"] + assert ( + ".github/workflows/trigger-oblt-aw-automerge.yml" in install["remove_files"] + ) dsts = {f["dst"] for f in install["files"]} - assert ".github/workflows/trigger-oblt-aw-automerge.yml" in dsts + assert ".github/workflows/trigger-oblt-aw.yml" in dsts + assert ".github/workflows/oblt-aw.yml" in dsts diff --git a/tests/test_oblt_aw_route_specs.py b/tests/test_oblt_aw_route_specs.py new file mode 100644 index 00000000..f6ddf8c5 --- /dev/null +++ b/tests/test_oblt_aw_route_specs.py @@ -0,0 +1,192 @@ +"""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 + default_workflow_file, + load_ingress_route_job_ids, + load_ingress_route_specs, + validate_all_org_registries, + validate_docs_ingress_registry, + validate_obs_ingress_registry, +) + + +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 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", + ) diff --git a/tests/test_validate_aw_workflow_prelude.py b/tests/test_validate_aw_workflow_prelude.py index 28c9b994..c5f228b5 100644 --- a/tests/test_validate_aw_workflow_prelude.py +++ b/tests/test_validate_aw_workflow_prelude.py @@ -5,49 +5,57 @@ 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_subject_workflows_includes_oblt_aw_wrappers() -> None: - names = {p.name for p in validator.list_subject_workflows()} +def test_list_workflow_files_includes_docs_and_oblt_wrappers() -> None: + names = {p.name for p in validator.list_workflow_files()} 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 "trg-oblt-aw-automerge.yml" not in names -def test_validate_workflow_rejects_missing_prelude( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_validate_aw_wrapper_rejects_prelude_job(tmp_path: pathlib.Path) -> None: workflows = tmp_path / ".github" / "workflows" workflows.mkdir(parents=True) - bad = workflows / "oblt-aw-test.yml" + bad = workflows / "docs-aw-test.yml" bad.write_text( - "name: Test\non:\n workflow_call:\njobs:\n run:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n", + "name: Test\non:\n workflow_call:\njobs:\n" + " prelude:\n uses: ./.github/workflows/aw-prelude.yml\n", encoding="utf-8", ) - monkeypatch.setattr(validator, "WORKFLOWS_DIR", workflows) - errors = validator.validate_workflow(bad) + errors = validator.validate_aw_wrapper_no_prelude(bad) assert len(errors) == 2 -def test_validate_workflow_accepts_prelude_job( - tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_validate_aw_wrapper_accepts_without_prelude(tmp_path: pathlib.Path) -> None: workflows = tmp_path / ".github" / "workflows" workflows.mkdir(parents=True) - good = workflows / "oblt-aw-test.yml" + good = workflows / "docs-aw-test.yml" good.write_text( "name: Test\non:\n workflow_call:\njobs:\n" - " prelude:\n uses: ./.github/workflows/aw-prelude.yml\n" - " with:\n control-plane-workflow: oblt-aw-test.yml\n", + " run:\n runs-on: ubuntu-latest\n steps:\n - run: echo hi\n", + encoding="utf-8", + ) + assert validator.validate_aw_wrapper_no_prelude(good) == [] + + +def test_validate_ingress_requires_prelude_and_route_jobs( + tmp_path: pathlib.Path, +) -> 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", encoding="utf-8", ) - monkeypatch.setattr(validator, "WORKFLOWS_DIR", workflows) - assert validator.validate_workflow(good) == [] + errors = validator.validate_ingress(bad) + assert any("route-*" in err for err in errors) diff --git a/tests/test_workflow_registry.py b/tests/test_workflow_registry.py index 0deff6b0..679a86ed 100644 --- a/tests/test_workflow_registry.py +++ b/tests/test_workflow_registry.py @@ -31,49 +31,28 @@ def _write_org( ) -class TestResolveCompoundId: - def test_single_file_entry(self, tmp_path: pathlib.Path) -> None: +class TestBuildControlPlaneWorkflowIndex: + def test_maps_files_to_compound_ids(self, tmp_path: pathlib.Path) -> None: _write_org( tmp_path, "obs", [ { "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", - [ + "ingress_routes": [{"id": "automerge"}], + }, { "id": "security", - "control_plane_workflows": [ - "oblt-aw-security-detector.yml", - "oblt-aw-security-fixer.yml", + "ingress_routes": [ + {"id": "security-detector"}, + {"id": "security-fixer"}, ], - } + }, ], ) - assert ( - wr.resolve_compound_id(tmp_path, "oblt-aw-security-fixer.yml") - == "obs:security" - ) - - def test_unknown_file_raises(self, tmp_path: pathlib.Path) -> None: - _write_org( - tmp_path, - "obs", - [{"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") + 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" class TestValidateRegistryAgainstWorkflows: @@ -83,7 +62,7 @@ def test_flags_missing_registry_entry( _write_org( tmp_path, "obs", - [{"id": "automerge", "control_plane_workflows": ["oblt-aw-automerge.yml"]}], + [{"id": "automerge", "ingress_routes": [{"id": "automerge"}]}], ) workflows = tmp_path / "workflows" workflows.mkdir()