Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/actions/verify-metrics-snapshot/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ runs:
restore-keys: |
${{ inputs.artifact_key }}

# Create an empty stub so the diff artifact is always uploaded on PRs.
# The fan-in uses 1-to-1 presence of diff artifacts to detect infra failures
# (a missing diff_* artifact means this action never ran for that snapshot).
# The stub is overwritten by the compare step when a baseline exists.
- name: Create diff file stub
if: github.ref_name != 'main'
shell: bash
run: touch ./.metrics/diff_${{ inputs.snapshot }}.txt

- name: Calculate diff between the snapshots
id: compare-snapshots
if: ${{ (github.ref_name != 'main') && (steps.download-release-snapshot.outputs.cache-matched-key != '') }}
Expand All @@ -61,8 +70,10 @@ runs:
echo "has_diff=true" >> $GITHUB_OUTPUT
fi

# Always upload the diff artifact on PRs (even when empty / no baseline yet).
# Presence of this artifact in the fan-in proves this action ran for the snapshot.
- name: Upload the diff artifact
if: ${{ (github.ref_name != 'main') && (steps.compare-snapshots.outputs.has_diff == 'true') }}
if: github.ref_name != 'main'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: diff_${{ inputs.artifact_key }}
Expand Down
14 changes: 0 additions & 14 deletions .github/workflows/ci-e2e-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,3 @@ jobs:

tailsampling:
uses: ./.github/workflows/ci-e2e-tailsampling.yml

upload_pr_number:
name: Save and Upload PR Number as Artifact
runs-on: ubuntu-latest
steps:
- name: Save PR number as artifact
if: github.event_name == 'pull_request'
run: echo "${{ github.event.number }}" > pr_number.txt
- name: Upload PR number artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: pr_number
path: pr_number.txt
237 changes: 102 additions & 135 deletions .github/workflows/ci-summary-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,117 +16,96 @@ on:
workflow_run:
workflows: ["CI Orchestrator"]
types: [completed]
workflow_dispatch:
inputs:
parent_run_id:
description: 'The Run ID of the CI workflow to pull artifacts from'
required: true
permissions:
contents: read
pull-requests: write
checks: write
jobs:
summary-report:
name: Summary Report
if: ${{ github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.head_branch == 'main' }}
if: |
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
ref: ${{ github.event.repository.default_branch }}
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.event.repository.default_branch }}

- name: Install adm-zip
run: npm install adm-zip

# Downloads all artifacts from the triggering CI Orchestrator run.
# Coverage artifacts (coverage-*) are extracted alongside metrics artifacts,
# landing at .artifacts/coverage-<name>/ for later merging.
# See: docs/adr/004-migrating-coverage-gating-to-github-actions.md (Step 4b)
- name: Download all artifacts from triggering workflow
id: download-artifacts
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { owner, repo } = context.repo;
const workflowRunId = context.payload.workflow_run.id;

// List all artifacts from the triggering workflow run
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: workflowRunId,
});
# Resolve the source run ID from either trigger, then fetch PR metadata.
# The run API includes associated PRs in .pull_requests[].
- name: Resolve source run
id: source-run
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
RUN_ID="${{ github.event.inputs.parent_run_id }}"
else
RUN_ID="${{ github.event.workflow_run.id }}"
fi
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "source_run_url=https://github.com/${{ github.repository }}/actions/runs/$RUN_ID" >> $GITHUB_OUTPUT
echo "summary_run_url=https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> $GITHUB_OUTPUT
Comment thread
yurishkuro marked this conversation as resolved.

// Download and extract each artifact
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
RUN_DATA=$(gh api "repos/${{ github.repository }}/actions/runs/$RUN_ID")

for (const artifact of artifacts.data.artifacts) {
const download = await github.rest.actions.downloadArtifact({
owner,
repo,
artifact_id: artifact.id,
archive_format: 'zip',
});
# Validate the parent run succeeded (guards against manual dispatch pointing at a failed run)
CONCLUSION=$(echo "$RUN_DATA" | jq -r '.conclusion')
if [ "$CONCLUSION" != "success" ]; then
echo "::error::Parent run $RUN_ID has conclusion '$CONCLUSION', expected 'success'."
exit 1
fi

const zip = new AdmZip(Buffer.from(download.data));
zip.extractAllTo(path.join(process.env.GITHUB_WORKSPACE, '.artifacts', artifact.name), true);
console.log(`Extracted artifact: ${artifact.name}`);
}
PR_NUMBER=$(echo "$RUN_DATA" | jq -r '.pull_requests[0].number')
HEAD_SHA=$(echo "$RUN_DATA" | jq -r '.pull_requests[0].head.sha')

// Extract PR number (adapted from PDF logic)
let prNumber = null;
const pullRequest = await github.rest.pulls.list({
owner,
repo,
head: `${context.payload.workflow_run.head_repository.full_name}:${context.payload.workflow_run.head_branch}`,
});
# For merge_group runs, .pull_requests[] may be empty; fall back to
# the run's own head_sha so check-runs attach to the correct commit.
if [ "$HEAD_SHA" == "null" ]; then
HEAD_SHA=$(echo "$RUN_DATA" | jq -r '.head_sha')
fi
echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT

const prArtifactPath = path.join(process.env.GITHUB_WORKSPACE, '.artifacts', 'pr_number', 'pr_number.txt');
if (fs.existsSync(prArtifactPath)) {
prNumber = fs.readFileSync(prArtifactPath, 'utf8').trim();
console.log(`Found PR Number from artifact: ${prNumber}`);
} else {
if (pullRequest.data.length > 0) {
prNumber = pullRequest.data[0].number;
} else {
// Fallback to commit SHA if needed
const commitSha = context.payload.workflow_run.head_sha;
const prsForCommit = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: commitSha,
});
if (prsForCommit.data.length > 0) {
prNumber = prsForCommit.data[0].number;
console.log(`Found PR via commit SHA: #${prNumber}`);
}
}
}
if [ "$PR_NUMBER" != "null" ]; then
echo "Found PR #$PR_NUMBER at SHA $HEAD_SHA"
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
else
echo "No associated PR found for run $RUN_ID; PR comment and check runs will be skipped."
fi
Comment thread
yurishkuro marked this conversation as resolved.

if (prNumber) {
console.log(`Found PR Number: ${prNumber}`);
core.setOutput('pr_number', prNumber);
} else {
console.log('Could not determine PR number; PR comment and check runs will be skipped.');
}
# Download all artifacts from the source CI run.
# Each artifact is extracted into .artifacts/<artifact-name>/.
- name: Download all artifacts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh run download "${{ steps.source-run.outputs.run_id }}" \
--repo "${{ github.repository }}" --dir .artifacts

- name: Install dependencies
if: success() && steps.download-artifacts.outputs.pr_number
if: success() && steps.source-run.outputs.pr_number
run: |
python3 -m pip install prometheus-client
npm install @actions/core @actions/github

Comment thread
yurishkuro marked this conversation as resolved.
- name: Compare metrics and generate summary
if: success() && steps.download-artifacts.outputs.pr_number
if: success() && steps.source-run.outputs.pr_number
id: compare-metrics
shell: bash
run: |
bash ./scripts/e2e/metrics_summary.sh
env:
LINK_TO_ARTIFACT: "https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}"
LINK_TO_ARTIFACT: ${{ steps.source-run.outputs.source_run_url }}
SUMMARY_RUN_URL: ${{ steps.source-run.outputs.summary_run_url }}

- name: Upload metrics comparison report as artifact
if: success() && steps.download-artifacts.outputs.pr_number
if: success() && steps.source-run.outputs.pr_number
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: metrics-comparison-report
Expand Down Expand Up @@ -166,6 +145,14 @@ jobs:
echo "skipped=false" >> "$GITHUB_OUTPUT"
fi

- name: Filter excluded paths from merged coverage
if: success() && steps.merge-coverage.outputs.skipped == 'false'
run: |
# Applies the same exclusions as .codecov.yml (single source of truth).
# filter_coverage.py modifies the file in-place.
python3 scripts/e2e/filter_coverage.py .artifacts/merged-coverage.out
echo "Coverage lines after filtering: $(wc -l < .artifacts/merged-coverage.out)"

- name: Calculate current coverage percentage
if: success() && steps.merge-coverage.outputs.skipped == 'false'
id: coverage
Expand All @@ -187,20 +174,20 @@ jobs:
coverage-baseline

- name: Gate on coverage regression
if: success() && steps.download-artifacts.outputs.pr_number && steps.merge-coverage.outputs.skipped == 'false'
if: success() && steps.source-run.outputs.pr_number && steps.merge-coverage.outputs.skipped == 'false'
Comment thread
yurishkuro marked this conversation as resolved.
Outdated
id: coverage-gate
run: |
CURRENT="${{ steps.coverage.outputs.percentage }}"
MINIMUM=95.0
BASELINE_MSG="(no baseline yet)"
failure_reasons=()

if [ -z "$CURRENT" ]; then
failure_reasons+=("coverage percentage is empty; go tool cover may have failed")
else
# Gate 1: absolute minimum (matches codecov.yml target: 95%)
# Gate 1: absolute minimum threshold
MINIMUM=95.0
if (( $(echo "$CURRENT < $MINIMUM" | bc -l) )); then
failure_reasons+=("coverage ${CURRENT}% is below required minimum ${MINIMUM}%")
failure_reasons+=("coverage ${CURRENT}% is below minimum ${MINIMUM}%")
fi

# Gate 2: no regression vs main baseline
Expand Down Expand Up @@ -228,7 +215,7 @@ jobs:
fi

- name: Append coverage section to combined summary
if: success() && steps.download-artifacts.outputs.pr_number && steps.merge-coverage.outputs.skipped == 'false'
if: success() && steps.source-run.outputs.pr_number && steps.merge-coverage.outputs.skipped == 'false'
run: |
{
echo ""
Expand All @@ -239,82 +226,60 @@ jobs:

- name: Post PR comment with combined summary
if: |
(steps.compare-metrics.outputs.TOTAL_CHANGES != '0' ||
steps.compare-metrics.outputs.HAS_ERROR == 'true' ||
(steps.compare-metrics.outputs.CONCLUSION == 'failure' ||
steps.compare-metrics.outputs.TOTAL_CHANGES != '0' ||
steps.coverage-gate.outputs.conclusion == 'failure') &&
steps.download-artifacts.outputs.pr_number
steps.source-run.outputs.pr_number
uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b # v3
with:
file-path: ./.artifacts/combined_summary.md
github-token: '${{ secrets.GITHUB_TOKEN }}'
comment-tag: "## CI Summary Report"
pr-number: ${{ steps.download-artifacts.outputs.pr_number }}
pr-number: ${{ steps.source-run.outputs.pr_number }}

- name: Create check run for metrics comparison
if: success() && steps.download-artifacts.outputs.pr_number
- name: Create check runs
if: success() && steps.source-run.outputs.pr_number
Comment thread
yurishkuro marked this conversation as resolved.
Outdated
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { owner, repo } = context.repo;
const headSha = context.payload.workflow_run.head_sha;
const totalChanges = parseInt('${{ steps.compare-metrics.outputs.TOTAL_CHANGES }}' || '0');
const hasError = '${{ steps.compare-metrics.outputs.HAS_ERROR }}' === 'true';
const artifactLink = 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}';

let conclusion, summary, text;

if (hasError) {
conclusion = 'failure';
summary = '❌ Metrics comparison failed';
text = 'ERROR: No summary files were generated. Expected at least 8 diff files from CI.\n\nThis indicates a failure in the E2E test execution or metrics collection process.\n\n➡️ [View full metrics file](' + artifactLink + ')';
} else if (totalChanges === 0) {
conclusion = 'success';
summary = '✅ No significant metric changes detected';
text = `Total changes across all snapshots: ${totalChanges}\n\n➡️ [View full metrics file](${artifactLink})`;
} else {
conclusion = 'failure';
summary = `❌ ${totalChanges} metric changes detected`;
text = `Total changes across all snapshots: ${totalChanges}\n\n➡️ [View full metrics file](${artifactLink})`;
}

const headSha = '${{ steps.source-run.outputs.head_sha }}';
const artifactLink = '${{ steps.source-run.outputs.source_run_url }}';
const summaryRunLink = '${{ steps.source-run.outputs.summary_run_url }}';
const links = `➡️ [View CI artifacts](${artifactLink}) | [View Summary Report logs](${summaryRunLink})`;

// Metrics comparison check
Comment thread
yurishkuro marked this conversation as resolved.
const metricsConclusion = '${{ steps.compare-metrics.outputs.CONCLUSION }}';
const metricsSummary = '${{ steps.compare-metrics.outputs.SUMMARY }}';
const totalChanges = '${{ steps.compare-metrics.outputs.TOTAL_CHANGES }}' || '0';
await github.rest.checks.create({
owner,
repo,
owner, repo, head_sha: headSha,
name: 'Metrics Comparison',
head_sha: headSha,
status: 'completed',
conclusion: conclusion,
conclusion: metricsConclusion,
output: {
title: 'Metrics Comparison Result',
summary: summary,
text: text
summary: metricsSummary,
text: `Total changes across all snapshots: ${totalChanges}\n\n${links}`
}
});
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create check run for coverage gate
if: success() && steps.download-artifacts.outputs.pr_number && steps.merge-coverage.outputs.skipped == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with:
script: |
const { owner, repo } = context.repo;
const headSha = context.payload.workflow_run.head_sha;
const conclusion = '${{ steps.coverage-gate.outputs.conclusion }}';
const summary = '${{ steps.coverage-gate.outputs.summary }}';
const artifactLink = 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}';

// Coverage gate check — always created so it works as a required status check.
// When no coverage data was collected, report success with a "skipped" note.
const coverageSkipped = '${{ steps.merge-coverage.outputs.skipped }}' !== 'false';
const coverageConclusion = coverageSkipped ? 'success' : '${{ steps.coverage-gate.outputs.conclusion }}';
const coverageSummary = coverageSkipped
? 'No coverage profiles found; coverage gate skipped.'
: '${{ steps.coverage-gate.outputs.summary }}';
await github.rest.checks.create({
owner,
repo,
owner, repo, head_sha: headSha,
name: 'Coverage Gate',
head_sha: headSha,
status: 'completed',
conclusion: conclusion,
conclusion: coverageConclusion,
output: {
title: 'Coverage Gate',
summary: conclusion === 'success' ? `✅ ${summary}` : `❌ ${summary}`,
text: `➡️ [View CI run](${artifactLink})`
summary: coverageSkipped ? `⏭️ ${coverageSummary}` : (coverageConclusion === 'success' ? `✅ ${coverageSummary}` : `❌ ${coverageSummary}`),
text: links
}
});
env:
Expand All @@ -324,12 +289,14 @@ jobs:
# Uses the same actions/cache pattern as .github/actions/verify-metrics-snapshot/action.yaml.
- name: Save coverage baseline on main branch
if: |
github.event_name == 'workflow_run' &&
github.event.workflow_run.head_branch == 'main' &&
steps.merge-coverage.outputs.skipped == 'false'
run: cp .artifacts/current-coverage.txt .artifacts/baseline-coverage.txt

- name: Cache coverage baseline
if: |
github.event_name == 'workflow_run' &&
github.event.workflow_run.head_branch == 'main' &&
steps.merge-coverage.outputs.skipped == 'false'
uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57
Expand Down
Loading
Loading