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
85 changes: 85 additions & 0 deletions .github/workflows/validate-content-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: Post content validation comment

# Runs after "Validate content" completes and posts the result as a PR comment.
#
# The pull_request trigger gives a read-only GITHUB_TOKEN for fork PRs — no
# permissions block can override that. workflow_run always runs in the base
# repo's context with a writable token, so it can post comments even when the
# originating PR came from a fork. The validation result is passed via artifact.

on:
workflow_run:
workflows: ["Validate content"]
types: [completed]

jobs:
comment:
runs-on: ubuntu-latest
# Only post comments for PR-triggered runs, not manual workflow_dispatch runs
if: github.event.workflow_run.event == 'pull_request'
permissions:
pull-requests: write
issues: write
actions: read # required to download artifacts from another workflow run

steps:
- name: Download PR info artifact
id: download
uses: actions/download-artifact@v4
continue-on-error: true # gracefully skip if artifact is missing (e.g. cancelled run)
with:
name: pr-info
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const marker = '<!-- validate-content-bot -->';
const fs = require('fs');

const prNumber = parseInt(fs.readFileSync('pr-number.txt', 'utf8').trim(), 10);
const branch = fs.readFileSync('branch.txt', 'utf8').trim();
const outcome = fs.readFileSync('outcome.txt', 'utf8').trim();

// No content files changed in this PR — nothing to comment on
if (outcome === 'skip') return;

let output = '';
try { output = fs.readFileSync('output.txt', 'utf8').trim(); } catch {}

// Detect if this PR was auto-generated from an issue
const issueMatch = branch.match(/^publish\/issue-(\d+)$/);
const linkedIssue = issueMatch ? issueMatch[1] : null;

const fixNote = linkedIssue
? `\n\nThis PR was auto-generated from issue #${linkedIssue}. [Edit the issue](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${linkedIssue}) to fix the errors above — removing and re-adding the \`approved\` label will regenerate the PR automatically.`
: `\n\nFix the errors above and push again — validation will re-run automatically.`;

const successBody = `${marker}\n✅ **Content validation passed!**`;
const failureBody = `${marker}\n## ❌ Content Validation Failed\n\n\`\`\`\n${output}\n\`\`\`${fixNote}`;
const body = outcome === 'failure' ? failureBody : successBody;

const { data: issueComments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = issueComments.find(c => c.body?.includes(marker));

if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
});
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
80 changes: 28 additions & 52 deletions .github/workflows/validate-content.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ on:
jobs:
validate:
runs-on: ubuntu-latest
# Read-only is intentional — write permissions are handled by the
# validate-content-comment workflow via workflow_run, so that fork PRs
# (which always get a read-only token on pull_request) can still get
# validation comments posted on them.
permissions:
pull-requests: write
contents: read

steps:
- uses: actions/checkout@v4
Expand All @@ -20,12 +24,6 @@ jobs:
with:
files: 'src/content/**/*.md'

- name: Skip if no content files changed
if: github.event_name == 'pull_request' && steps.changed.outputs.any_changed != 'true'
run: |
echo "No content files changed — skipping validation."
exit 0

- uses: actions/setup-node@v4
if: github.event_name == 'workflow_dispatch' || steps.changed.outputs.any_changed == 'true'
with:
Expand All @@ -47,52 +45,30 @@ jobs:
if: github.event_name == 'workflow_dispatch'
run: npx tsx scripts/validate-content.ts

- name: Post or update PR comment
if: github.event_name == 'pull_request' && steps.changed.outputs.any_changed == 'true'
uses: actions/github-script@v7
# Save PR metadata as an artifact so validate-content-comment can read it.
# Must run before "Fail if validation failed" so the artifact is always uploaded
# even when validation fails.
- name: Save PR metadata for comment workflow
if: github.event_name == 'pull_request'
run: |
mkdir -p /tmp/pr-info
echo "${{ github.event.pull_request.number }}" > /tmp/pr-info/pr-number.txt
echo "${{ github.event.pull_request.head.ref }}" > /tmp/pr-info/branch.txt
if [ "${{ steps.changed.outputs.any_changed }}" != "true" ]; then
echo "skip" > /tmp/pr-info/outcome.txt
echo "" > /tmp/pr-info/output.txt
else
echo "${{ steps.validate.outcome }}" > /tmp/pr-info/outcome.txt
cp /tmp/validate-output.txt /tmp/pr-info/output.txt 2>/dev/null || echo "" > /tmp/pr-info/output.txt
fi

- name: Upload PR metadata artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
script: |
const marker = '<!-- validate-content-bot -->';
const outcome = '${{ steps.validate.outcome }}';
const fs = require('fs');

let output = '';
try { output = fs.readFileSync('/tmp/validate-output.txt', 'utf8').trim(); } catch {}

// Detect if this PR was auto-generated from an issue
const branchName = context.payload.pull_request?.head?.ref || '';
const issueMatch = branchName.match(/^publish\/issue-(\d+)$/);
const linkedIssue = issueMatch ? issueMatch[1] : null;

const fixNote = linkedIssue
? `\n\nThis PR was auto-generated from issue #${linkedIssue}. [Edit the issue](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${linkedIssue}) to fix the errors above — removing and re-adding the \`approved\` label will regenerate the PR automatically.`
: `\n\nFix the errors above and push again — validation will re-run automatically.`;

const successBody = `${marker}\n✅ **Content validation passed!**`;
const failureBody = `${marker}\n## ❌ Content Validation Failed\n\n\`\`\`\n${output}\n\`\`\`${fixNote}`;
const body = outcome === 'failure' ? failureBody : successBody;

const { data: issueComments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = issueComments.find(c => c.body?.includes(marker));

if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
});
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
name: pr-info
path: /tmp/pr-info/
retention-days: 1

- name: Fail if validation failed
if: steps.validate.outcome == 'failure'
Expand Down
Loading