Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
213 changes: 2 additions & 211 deletions .github/workflows/cgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Verify CGO/CJS workflow purity
run: bash scripts/check-cgo-cjs-workflow-purity.sh
- name: Cache repository checkout
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
Expand Down Expand Up @@ -2508,216 +2510,6 @@ jobs:
path: conformance-output.txt
retention-days: 7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] The notify-failure job is deleted without a documented decision or replacement strategy — if failure alerting on main is still needed, the rationale for removing it is not captured.

💡 Suggestion

Either:

  • Add a short comment in the PR body or a commit message noting that failure notification is intentionally dropped (e.g., superseded by another alerting system), or
  • File a follow-up issue tracking the gap if alerting was valuable.

The deletion is a significant loss of observability. Without a record, a future contributor may re-add the exact pattern that introduced the issues: write permission.

@copilot please address this.

notify-failure:
name: Notify on CGO Failure
runs-on: ubuntu-latest
timeout-minutes: 5
if: always() && github.ref == 'refs/heads/main'
needs:
- checkout-cache
- verify-integration-build
- test
- canary-go
- build
- build-wasm
- bench
- check-validator-sizes
- lint-go
- lint-error-messages
- actions-build
- fuzz
- security
- security-scan
- mcp-server-compile-test
- cross-platform-build
- alpine-container-test
- safe-outputs-conformance
permissions:
issues: write
steps:
- name: Check for job failures and create issue
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const needs = ${{ toJSON(needs) }};
const failedJobs = Object.entries(needs)
.filter(([, job]) => job.result === 'failure')
.map(([name]) => name);
const hasFuzzFailure = failedJobs.some(name => name === 'fuzz' || name.startsWith('fuzz-'));

if (failedJobs.length === 0) {
core.info('No jobs failed. Nothing to do.');
return;
}

core.info(`Failed jobs: ${failedJobs.join(', ')}`);

// Check for an existing open CGO failure issue to avoid duplicates
const existingIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'cgo-failure',
state: 'open',
});

if (existingIssues.data.length > 0 && !hasFuzzFailure) {
core.info(`Existing CGO failure issue #${existingIssues.data[0].number} is still open. Skipping.`);
return;
}

if (hasFuzzFailure) {
const existingFuzzIssues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
labels: 'cgo-fuzz-failure',
state: 'open',
});

const existingFuzzIssueForRun = existingFuzzIssues.data.find(issue =>
issue.title.includes(`Run #${context.runNumber}`),
);
if (existingFuzzIssueForRun) {
core.info(`Fuzz failure issue #${existingFuzzIssueForRun.number} already exists for run #${context.runNumber}. Skipping.`);
return;
}
}

// Ensure required labels exist, creating them if missing
const requiredLabels = [['cookie', 'e4e669'], ['cgo-failure', 'b60205']];
if (hasFuzzFailure) {
requiredLabels.push(['cgo-fuzz-failure', 'd93f0b']);
}

for (const [label, color] of requiredLabels) {
try {
await github.rest.issues.getLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label });
} catch (e) {
if (e.status === 404) {
await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, name: label, color });
}
}
}

// Fetch all jobs for this run to get direct job links
const jobsResponse = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.runId,
per_page: 100,
});

// Build a map from job name to job URL for failed jobs
const jobUrlMap = {};
for (const job of jobsResponse.data.jobs) {
if (failedJobs.includes(job.name)) {
jobUrlMap[job.name] = job.html_url;
}
}

const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const expiresAt = new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString();

const parseUTCOffsetMinutes = (rawOffset) => {
const offset = typeof rawOffset === 'string' ? rawOffset.trim() : '';
const match = offset.match(/^([+-])(\d{2}):(\d{2})$/);
if (!match) return Number.NaN;
const [, sign, hoursText, minutesText] = match;
const hours = Number.parseInt(hoursText, 10);
const minutes = Number.parseInt(minutesText, 10);
if (hours > 14 || minutes > 59 || (hours === 14 && minutes !== 0)) {
return Number.NaN;
}
const direction = sign === '-' ? -1 : 1;
return direction * (hours * 60 + minutes);
};

let repoUTCOffset = '';
try {
const awConfigFile = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: '.github/workflows/aw.json',
ref: context.sha,
});
if (!Array.isArray(awConfigFile.data) && awConfigFile.data.type === 'file' && awConfigFile.data.content) {
const awConfigRaw = Buffer.from(awConfigFile.data.content, 'base64').toString('utf8');
const awConfig = JSON.parse(awConfigRaw);
const rawUTC = typeof awConfig?.utc === 'string' ? awConfig.utc.trim() : '';
if (!Number.isNaN(parseUTCOffsetMinutes(rawUTC))) {
repoUTCOffset = rawUTC;
} else if (rawUTC) {
core.warning(`Ignoring invalid utc offset in .github/workflows/aw.json: ${rawUTC}`);
}
}
} catch (error) {
core.warning(`Unable to read .github/workflows/aw.json UTC offset: ${error?.message || String(error)}`);
}

// Format expiration line using the gh-aw-expires XML comment format
const expiresDate = new Date(expiresAt);
const repoOffsetMinutes = parseUTCOffsetMinutes(repoUTCOffset);
const hasRepoUTCOffset = !Number.isNaN(repoOffsetMinutes);
const displayDate = hasRepoUTCOffset
? new Date(expiresDate.getTime() + repoOffsetMinutes * 60 * 1000)
: expiresDate;
const humanReadableDate = displayDate.toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
});
const humanReadableSuffix = hasRepoUTCOffset ? `UTC${repoUTCOffset}` : 'UTC';
const expirationLine = `- [x] expires <!-- gh-aw-expires: ${expiresAt} --> on ${humanReadableDate} ${humanReadableSuffix}`;

const body = [
`## CGO Workflow Failure`,
``,
`Workflow run [#${context.runNumber}](${runUrl}) on the \`main\` branch completed with failed jobs.`,
``,
`| Field | Value |`,
`| --- | --- |`,
`| Run ID | ${context.runId} |`,
`| Commit | ${context.sha} |`,
`| Expires | ${expiresAt} |`,
``,
`## Failed Jobs`,
``,
// Map job names to direct links; fall back to plain text if a job ID wasn't found
...failedJobs.map(name => jobUrlMap[name]
? `- [\`${name}\`](${jobUrlMap[name]})`
: `- \`${name}\``),
``,
`> This issue expires at ${expiresAt}. Please investigate the failed jobs above and close once resolved.`,
`> ${expirationLine}`,
];

if (hasFuzzFailure) {
body.splice(
4,
0,
`Detected failure in the \`fuzz\` job matrix. A dedicated fuzz failure label was applied so this run is tracked.`,
``,
);
}

const issueBody = body.join('\n');

const issueLabels = ['cookie', 'cgo-failure'];
if (hasFuzzFailure) {
issueLabels.push('cgo-fuzz-failure');
}

const issue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: hasFuzzFailure
? `[CGO][FUZZ] Workflow failure on main - Run #${context.runNumber}`
: `[CGO] Workflow failure on main - Run #${context.runNumber}`,
body: issueBody,
labels: issueLabels,
});

core.info(`Created issue #${issue.data.number}: ${issue.data.html_url}`);

summarize-timing:
name: Summarize workflow timing
needs:
Expand All @@ -2741,7 +2533,6 @@ jobs:
- cross-platform-build
- alpine-container-test
- safe-outputs-conformance
- notify-failure
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/cjs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- 'actions/setup/md/**'
- 'pkg/cli/data/models.json'
- 'scripts/**/*.js'
- 'scripts/check-cgo-cjs-workflow-purity.sh'
- 'Makefile'
- '.github/workflows/ci.yml'
- '.github/workflows/cjs.yml'
Expand All @@ -18,6 +19,7 @@ on:
- 'actions/setup/md/**'
- 'pkg/cli/data/models.json'
- 'scripts/**/*.js'
- 'scripts/check-cgo-cjs-workflow-purity.sh'
- 'Makefile'
- '.github/workflows/ci.yml'
- '.github/workflows/cjs.yml'
Expand All @@ -35,6 +37,8 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Verify CGO/CJS workflow purity
run: bash scripts/check-cgo-cjs-workflow-purity.sh
- name: Cache repository checkout
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
Expand Down Expand Up @@ -202,7 +206,7 @@ jobs:
timeout-minutes: 10
permissions:
contents: read
actions: write
actions: read
concurrency:
group: ci-${{ github.ref }}-artifact-integration
cancel-in-progress: true
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,13 @@ bundle-js:
@echo "✓ bundle-js tool built"
@echo "To bundle a JavaScript file: ./bundle-js <input-file> [output-file]"

# Run Bash script tests (check-stale-lock-files, check-workflow-drift)
# Run Bash script tests (check-stale-lock-files, check-workflow-drift, check-cgo-cjs-workflow-purity)
.PHONY: test-scripts
test-scripts: build
@echo "Running Bash script tests..."
bash scripts/check-stale-lock-files_test.sh
bash scripts/check-workflow-drift_test.sh ./$(BINARY_NAME)
bash scripts/check-cgo-cjs-workflow-purity_test.sh
@echo "✓ All Bash script tests passed"

# Test all code (Go, JavaScript, wasm golden, and shell scripts)
Expand Down
Loading
Loading