ci: report safe-to-test CI results on PR (#807)

* feat(api): warn on unknown request parameters via X-Ignored-Params header

Add middleware that detects unknown query params and JSON body fields,
logs a server-side warning, and returns an X-Ignored-Params response
header listing the ignored parameters. This surfaces silent parameter
ignoring (e.g. tag=source:slack on /memories/list) without breaking
forward compatibility between client and server versions.

Closes #792

* ci: report safe-to-test CI results on PR via status and comment

pull_request_target workflow runs are not linked to the PR by GitHub,
so the CI results are invisible on the PR page after adding safe-to-test.

Add a report-pr-status job that:
- Creates a commit status on the PR head SHA
- Posts/updates a summary comment with pass/fail counts and failed job names

* ci: skip secret-dependent jobs on fork pull_request events

Adds a has_secrets output to detect-changes that is false for fork PRs
via pull_request events. All 15 secret-dependent jobs now check this
output before running, avoiding guaranteed failures on fork PRs.

Fork contributors will see these jobs as skipped instead of failed,
and can use the safe-to-test label to run the full CI suite.
This commit is contained in:
Nicolò Boschi 2026-03-31 11:09:24 +02:00 committed by GitHub
parent cef42d8154
commit 1dbbe39ea1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -48,6 +48,9 @@ jobs:
integrations-llamaindex: ${{ steps.filter.outputs.integrations-llamaindex }}
dev: ${{ steps.filter.outputs.dev }}
ci: ${{ steps.filter.outputs.ci }}
# Secrets are available for internal PRs, pull_request_target, and workflow_dispatch.
# Fork PRs via pull_request event do NOT have access to secrets.
has_secrets: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
steps:
- uses: actions/checkout@v6
with:
@ -446,11 +449,12 @@ jobs:
test-rust-cli:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.cli == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -597,12 +601,13 @@ jobs:
build-docker-images:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.docker == 'true' ||
needs.detect-changes.outputs.control-plane == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
name: Build Docker (${{ matrix.name }})
runs-on: ubuntu-latest
strategy:
@ -690,10 +695,11 @@ jobs:
test-api:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -766,11 +772,12 @@ jobs:
test-python-client:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.clients-python == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -877,11 +884,12 @@ jobs:
test-typescript-client:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.clients-ts == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -993,11 +1001,12 @@ jobs:
test-typescript-client-deno:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.clients-ts == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1147,11 +1156,12 @@ jobs:
test-rust-client:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.clients-rust == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1262,11 +1272,12 @@ jobs:
test-go-client:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.clients-go == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1375,12 +1386,13 @@ jobs:
test-openclaw-integration:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.integrations-openclaw == 'true' ||
needs.detect-changes.outputs.embed == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1492,11 +1504,12 @@ jobs:
test-integration:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.integration-tests == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1820,10 +1833,11 @@ jobs:
test-pip-slim:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1888,11 +1902,12 @@ jobs:
test-embed:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.embed == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -1950,11 +1965,12 @@ jobs:
test-hindsight-all:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.hindsight-all == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -2034,7 +2050,8 @@ jobs:
test-doc-examples:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.clients-ts == 'true' ||
@ -2042,7 +2059,7 @@ jobs:
needs.detect-changes.outputs.clients-go == 'true' ||
needs.detect-changes.outputs.cli == 'true' ||
needs.detect-changes.outputs.docs == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
strategy:
fail-fast: false
@ -2186,11 +2203,12 @@ jobs:
test-upgrade:
needs: [detect-changes]
if: >-
(github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
needs.detect-changes.outputs.has_secrets == 'true' &&
((github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test') ||
github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.core == 'true' ||
needs.detect-changes.outputs.dev == 'true' ||
needs.detect-changes.outputs.ci == 'true'
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
env:
HINDSIGHT_API_LLM_PROVIDER: vertexai
@ -2394,4 +2412,131 @@ jobs:
# Check compatibility using our tool
cd hindsight-dev
uv run check-openapi-compatibility /tmp/old-openapi.json ../hindsight-docs/static/openapi.json
uv run check-openapi-compatibility /tmp/old-openapi.json ../hindsight-docs/static/openapi.json
# Report CI status back to the PR for pull_request_target events.
# GitHub does not automatically link pull_request_target check runs to the PR,
# so we create a commit status on the PR head SHA explicitly.
report-pr-status:
if: github.event_name == 'pull_request_target' && github.event.label.name == 'safe-to-test' && always()
needs:
- detect-changes
- build-api-python-versions
- build-typescript-client
- build-openclaw-integration
- test-claude-code-integration
- test-codex-integration
- build-ai-sdk-integration
- test-ai-sdk-integration-deno
- build-chat-integration
- build-control-plane
- build-docs
- test-rust-cli
- lint-helm-chart
- build-docker-images
- test-api
- test-python-client
- test-typescript-client
- test-typescript-client-deno
- build-rust-cli-arm64
- test-rust-client
- test-go-client
- test-openclaw-integration
- test-integration
- test-ag2-integration
- test-crewai-integration
- test-litellm-integration
- test-pydantic-ai-integration
- test-hermes-integration
- test-llamaindex-integration
- test-pip-slim
- test-embed
- test-hindsight-all
- test-doc-examples
- test-upgrade
- verify-generated-files
- check-openapi-compatibility
runs-on: ubuntu-latest
permissions:
statuses: write
pull-requests: write
steps:
- name: Determine overall result
id: result
uses: actions/github-script@v7
with:
script: |
const needs = ${{ toJSON(needs) }};
const entries = Object.entries(needs);
const failed = entries.filter(([, v]) => v.result === 'failure');
const skipped = entries.filter(([, v]) => v.result === 'skipped');
const succeeded = entries.filter(([, v]) => v.result === 'success');
const overall = failed.length > 0 ? 'failure' : 'success';
const icon = overall === 'success' ? '✅' : '❌';
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
let body = `## ${icon} safe-to-test CI results\n\n`;
body += `| Status | Count |\n|--------|-------|\n`;
body += `| ✅ Passed | ${succeeded.length} |\n`;
if (failed.length > 0) body += `| ❌ Failed | ${failed.length} |\n`;
if (skipped.length > 0) body += `| ⏭️ Skipped | ${skipped.length} |\n`;
if (failed.length > 0) {
body += `\n### Failed jobs\n`;
for (const [name] of failed) {
body += `- \`${name}\`\n`;
}
}
body += `\n[View full run](${runUrl})`;
core.setOutput('state', overall);
core.setOutput('description', failed.length > 0 ? 'Some CI checks failed' : 'All CI checks passed');
core.setOutput('body', body);
core.setOutput('run_url', runUrl);
- name: Report status to PR
uses: actions/github-script@v7
with:
script: |
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.payload.pull_request.head.sha,
state: '${{ steps.result.outputs.state }}',
context: 'CI / safe-to-test',
description: '${{ steps.result.outputs.description }}',
target_url: '${{ steps.result.outputs.run_url }}'
});
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.pull_request.number;
const marker = '<!-- safe-to-test-ci-report -->';
const body = `${marker}\n${{ steps.result.outputs.body }}`;
// Update existing comment if present, otherwise create new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}