mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-11 02:35:55 +00:00
Nightly report: post details as follow-up comments instead of truncating (#1239)
When the full report exceeds GitHub's 65K body limit, the summary table stays in the discussion/issue body and the verbose skill/agent output is posted as follow-up comments (split into chunks if needed). This ensures no output is lost.
This commit is contained in:
198
.github/workflows/skill-quality-report.yml
vendored
198
.github/workflows/skill-quality-report.yml
vendored
@@ -206,94 +206,96 @@ jobs:
|
|||||||
|
|
||||||
const title = `Skill Quality Report — ${today}`;
|
const title = `Skill Quality Report — ${today}`;
|
||||||
|
|
||||||
const body = [
|
const annotatedSkills = annotateWithAuthors(skillsOutput, 'skill');
|
||||||
`# ${title}`,
|
const annotatedAgents = annotateWithAuthors(agentsOutput, 'agent');
|
||||||
'',
|
|
||||||
`**${skillDirs.length} skills** and **${agentFiles.length} agents** scanned.`,
|
// ── Body size management ──────────────────────────────
|
||||||
'',
|
// GitHub body limit is ~65536 UTF-8 bytes for both
|
||||||
`| Severity | Count |`,
|
// Discussions and Issues. When the full report fits, we
|
||||||
`|----------|-------|`,
|
// inline everything. When it doesn't, the body gets a
|
||||||
|
// compact summary and the verbose sections are written to
|
||||||
|
// separate files that get posted as follow-up comments.
|
||||||
|
const MAX_BYTES = 65000; // leave margin
|
||||||
|
|
||||||
|
function makeDetailsBlock(heading, summary, content) {
|
||||||
|
return [
|
||||||
|
`## ${heading}`, '',
|
||||||
|
'<details>',
|
||||||
|
`<summary>${summary}</summary>`, '',
|
||||||
|
'```', content, '```', '',
|
||||||
|
'</details>',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryLines = [
|
||||||
|
`# ${title}`, '',
|
||||||
|
`**${skillDirs.length} skills** and **${agentFiles.length} agents** scanned.`, '',
|
||||||
|
'| Severity | Count |',
|
||||||
|
'|----------|-------|',
|
||||||
`| ⛔ Errors | ${errorCount} |`,
|
`| ⛔ Errors | ${errorCount} |`,
|
||||||
`| ⚠️ Warnings | ${warningCount} |`,
|
`| ⚠️ Warnings | ${warningCount} |`,
|
||||||
`| ℹ️ Advisories | ${advisoryCount} |`,
|
`| ℹ️ Advisories | ${advisoryCount} |`, '',
|
||||||
'',
|
|
||||||
'---',
|
'---',
|
||||||
'',
|
];
|
||||||
'## Skills',
|
const footer = `\n---\n\n_Generated by the [Skill Validator nightly scan](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/skill-quality-report.yml)._`;
|
||||||
'',
|
|
||||||
'<details>',
|
const skillsBlock = makeDetailsBlock('Skills', 'Full skill-validator output for skills', annotatedSkills);
|
||||||
'<summary>Full skill-validator output for skills</summary>',
|
const agentsBlock = makeDetailsBlock('Agents', 'Full skill-validator output for agents', annotatedAgents);
|
||||||
'',
|
|
||||||
'```',
|
// Try full inline body first
|
||||||
annotateWithAuthors(skillsOutput, 'skill'),
|
const fullBody = summaryLines.join('\n') + '\n\n' + skillsBlock + '\n\n' + agentsBlock + footer;
|
||||||
'```',
|
|
||||||
'',
|
const commentParts = []; // overflow comment files
|
||||||
'</details>',
|
|
||||||
'',
|
let finalBody;
|
||||||
'## Agents',
|
if (Buffer.byteLength(fullBody, 'utf8') <= MAX_BYTES) {
|
||||||
'',
|
finalBody = fullBody;
|
||||||
'<details>',
|
} else {
|
||||||
'<summary>Full skill-validator output for agents</summary>',
|
// Details won't fit inline — move them to follow-up comments
|
||||||
'',
|
const bodyNote = '\n\n> **Note:** Detailed output is posted in the comments below (too large for the discussion body).\n';
|
||||||
'```',
|
finalBody = summaryLines.join('\n') + bodyNote + footer;
|
||||||
annotateWithAuthors(agentsOutput, 'agent'),
|
|
||||||
'```',
|
// Split each section into ≤65 KB chunks
|
||||||
'',
|
function chunkContent(label, content) {
|
||||||
'</details>',
|
const prefix = `## ${label}\n\n\`\`\`\n`;
|
||||||
'',
|
const suffix = '\n```';
|
||||||
'---',
|
const overhead = Buffer.byteLength(prefix + suffix, 'utf8');
|
||||||
'',
|
const budget = MAX_BYTES - overhead;
|
||||||
`_Generated by the [Skill Validator nightly scan](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/skill-quality-report.yml)._`,
|
|
||||||
].join('\n');
|
const buf = Buffer.from(content, 'utf8');
|
||||||
|
if (buf.length <= budget) {
|
||||||
|
return [prefix + content + suffix];
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
let offset = 0;
|
||||||
|
let partNum = 1;
|
||||||
|
while (offset < buf.length) {
|
||||||
|
const slice = buf.slice(offset, offset + budget).toString('utf8');
|
||||||
|
// Remove trailing replacement char from mid-codepoint cut
|
||||||
|
const clean = slice.replace(/\uFFFD$/, '');
|
||||||
|
const hdr = `## ${label} (part ${partNum})\n\n\`\`\`\n`;
|
||||||
|
parts.push(hdr + clean + suffix);
|
||||||
|
offset += Buffer.byteLength(clean, 'utf8');
|
||||||
|
partNum++;
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
commentParts.push(...chunkContent('Skills', annotatedSkills));
|
||||||
|
commentParts.push(...chunkContent('Agents', annotatedAgents));
|
||||||
|
}
|
||||||
|
|
||||||
core.setOutput('title', title);
|
core.setOutput('title', title);
|
||||||
core.setOutput('body_file', 'report-body.md');
|
core.setOutput('body_file', 'report-body.md');
|
||||||
|
|
||||||
// GitHub Issues/Discussions enforce a body size limit on the
|
|
||||||
// UTF-8 payload (~65536 bytes). Use byte-based limits and prefer
|
|
||||||
// shrinking verbose <details> sections to keep markdown valid.
|
|
||||||
const MAX_BODY_BYTES = 65000; // leave some margin
|
|
||||||
|
|
||||||
function shrinkDetailsSections(markdown) {
|
|
||||||
return markdown.replace(
|
|
||||||
/<details([\s\S]*?)>[\s\S]*?<\/details>/g,
|
|
||||||
(match, attrs) => {
|
|
||||||
const placeholder = '\n<summary>Details truncated</summary>\n\n' +
|
|
||||||
"> Full output was truncated to fit GitHub's body size limit. " +
|
|
||||||
'See the workflow run for complete output.\n';
|
|
||||||
return `<details${attrs}>${placeholder}</details>`;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function trimToByteLimit(str, maxBytes) {
|
|
||||||
const buf = Buffer.from(str, 'utf8');
|
|
||||||
if (buf.length <= maxBytes) return str;
|
|
||||||
// Slice bytes and decode, which safely handles multi-byte chars
|
|
||||||
return buf.slice(0, maxBytes).toString('utf8').replace(/\uFFFD$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
const truncNote = '\n\n> **Note:** Output was truncated to fit GitHub\'s body size limit. See the [workflow run](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/workflows/skill-quality-report.yml) for full output.\n';
|
|
||||||
const truncNoteBytes = Buffer.byteLength(truncNote, 'utf8');
|
|
||||||
|
|
||||||
let finalBody = body;
|
|
||||||
|
|
||||||
if (Buffer.byteLength(finalBody, 'utf8') > MAX_BODY_BYTES) {
|
|
||||||
// First try: collapse <details> sections to reduce size
|
|
||||||
finalBody = shrinkDetailsSections(finalBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Buffer.byteLength(finalBody, 'utf8') > MAX_BODY_BYTES) {
|
|
||||||
// Last resort: hard byte-trim + truncation note
|
|
||||||
finalBody = trimToByteLimit(finalBody, MAX_BODY_BYTES - truncNoteBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Buffer.byteLength(finalBody, 'utf8') < Buffer.byteLength(body, 'utf8')) {
|
|
||||||
finalBody += truncNote;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync('report-body.md', finalBody);
|
fs.writeFileSync('report-body.md', finalBody);
|
||||||
|
|
||||||
|
// Write overflow comment parts as numbered files
|
||||||
|
for (let i = 0; i < commentParts.length; i++) {
|
||||||
|
fs.writeFileSync(`report-comment-${i}.md`, commentParts[i]);
|
||||||
|
}
|
||||||
|
core.setOutput('comment_count', String(commentParts.length));
|
||||||
|
|
||||||
# ── Create Discussion (preferred) or Issue (fallback) ────────
|
# ── Create Discussion (preferred) or Issue (fallback) ────────
|
||||||
- name: Create Discussion
|
- name: Create Discussion
|
||||||
id: create-discussion
|
id: create-discussion
|
||||||
@@ -304,6 +306,7 @@ jobs:
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const title = '${{ steps.report.outputs.title }}'.replace(/'/g, "\\'");
|
const title = '${{ steps.report.outputs.title }}'.replace(/'/g, "\\'");
|
||||||
const body = fs.readFileSync('report-body.md', 'utf8');
|
const body = fs.readFileSync('report-body.md', 'utf8');
|
||||||
|
const commentCount = parseInt('${{ steps.report.outputs.comment_count }}' || '0', 10);
|
||||||
|
|
||||||
// Find the "Skill Quality Reports" category
|
// Find the "Skill Quality Reports" category
|
||||||
const categoriesResult = await github.graphql(`
|
const categoriesResult = await github.graphql(`
|
||||||
@@ -331,7 +334,7 @@ jobs:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await github.graphql(`
|
const result = await github.graphql(`
|
||||||
mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
|
mutation($repoId: ID!, $categoryId: ID!, $title: String!, $body: String!) {
|
||||||
createDiscussion(input: {
|
createDiscussion(input: {
|
||||||
repositoryId: $repoId,
|
repositoryId: $repoId,
|
||||||
@@ -339,7 +342,7 @@ jobs:
|
|||||||
title: $title,
|
title: $title,
|
||||||
body: $body
|
body: $body
|
||||||
}) {
|
}) {
|
||||||
discussion { url }
|
discussion { id url }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, {
|
`, {
|
||||||
@@ -349,7 +352,24 @@ jobs:
|
|||||||
body: body,
|
body: body,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Discussion created successfully.');
|
const discussionId = result.createDiscussion.discussion.id;
|
||||||
|
console.log(`Discussion created: ${result.createDiscussion.discussion.url}`);
|
||||||
|
|
||||||
|
// Post overflow detail comments
|
||||||
|
for (let i = 0; i < commentCount; i++) {
|
||||||
|
const commentBody = fs.readFileSync(`report-comment-${i}.md`, 'utf8');
|
||||||
|
await github.graphql(`
|
||||||
|
mutation($discussionId: ID!, $body: String!) {
|
||||||
|
addDiscussionComment(input: {
|
||||||
|
discussionId: $discussionId,
|
||||||
|
body: $body
|
||||||
|
}) {
|
||||||
|
comment { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, { discussionId, body: commentBody });
|
||||||
|
console.log(`Posted detail comment ${i + 1}/${commentCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
- name: Fallback — Create Issue
|
- name: Fallback — Create Issue
|
||||||
if: steps.create-discussion.outcome == 'failure'
|
if: steps.create-discussion.outcome == 'failure'
|
||||||
@@ -358,7 +378,17 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
# Create label if it doesn't exist (ignore errors if it already exists)
|
# Create label if it doesn't exist (ignore errors if it already exists)
|
||||||
gh label create "skill-quality" --description "Automated skill quality reports" --color "d4c5f9" 2>/dev/null || true
|
gh label create "skill-quality" --description "Automated skill quality reports" --color "d4c5f9" 2>/dev/null || true
|
||||||
gh issue create \
|
ISSUE_URL=$(gh issue create \
|
||||||
--title "${{ steps.report.outputs.title }}" \
|
--title "${{ steps.report.outputs.title }}" \
|
||||||
--body-file report-body.md \
|
--body-file report-body.md \
|
||||||
--label "skill-quality"
|
--label "skill-quality")
|
||||||
|
echo "Created issue: $ISSUE_URL"
|
||||||
|
|
||||||
|
# Post overflow detail comments on the issue
|
||||||
|
COMMENT_COUNT=${{ steps.report.outputs.comment_count }}
|
||||||
|
for i in $(seq 0 $(( ${COMMENT_COUNT:-0} - 1 ))); do
|
||||||
|
if [ -f "report-comment-${i}.md" ]; then
|
||||||
|
gh issue comment "$ISSUE_URL" --body-file "report-comment-${i}.md"
|
||||||
|
echo "Posted detail comment $((i+1))/${COMMENT_COUNT}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user