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
80 changes: 80 additions & 0 deletions .github/workflows/comment-on-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Comment on plugin PR

# Posts the validation result produced by "Validate plugin submission" back to
# the PR. That workflow runs in the PR (fork) context with a read-only token
# and therefore can't comment itself. This workflow runs via workflow_run in
# the base-repo context, where the token can write to issues/PRs. It only reads
# an artifact — it never checks out or executes PR code — so it is safe to grant
# write permissions even though the triggering run was for untrusted fork code.

on:
workflow_run:
workflows: ["Validate plugin submission"]
types:
- completed

permissions:
contents: read
actions: read
issues: write
pull-requests: write

jobs:
comment:
runs-on: ubuntu-latest
# Only act on runs triggered by pull requests.
if: github.event.workflow_run.event == 'pull_request'
steps:
- name: Download comment artifact
id: download
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: pr-comment
path: pr-comment
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Post or update comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');

const body = fs.readFileSync('pr-comment/body.md', 'utf8');
const issue_number = parseInt(
fs.readFileSync('pr-comment/pr.txt', 'utf8').trim(),
10,
);
if (!Number.isInteger(issue_number)) {
core.setFailed('No valid PR number in artifact.');
return;
}

const { owner, repo } = context.repo;

const { data: comments } = await github.rest.issues.listComments({
owner,
repo,
issue_number,
});

const marker = '## Plugin Validation';
const existing = comments.find((c) => c.body.startsWith(marker));

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
73 changes: 40 additions & 33 deletions .github/workflows/validate-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ on:
paths:
- 'plugins/**'

# This workflow runs in the PR's (fork) context, so it gets a read-only token
# and must not need write access. The validation result comment is posted by
# the separate comment-on-pr.yml workflow, which runs in the base-repo context
# with write permissions. See that file for why the split is necessary.
permissions:
pull-requests: write
contents: read

jobs:
validate:
Expand Down Expand Up @@ -225,18 +229,35 @@ jobs:
exit 1
fi

- name: Comment on PR
# We can't post the result comment from here: on fork PRs the
# GITHUB_TOKEN is read-only, so any issues/PR write returns 403. Instead
# we render the comment body to a file and hand it off as an artifact to
# comment-on-pr.yml, which runs via workflow_run with a writable token.
- name: Render PR comment
id: render
if: always() && steps.changes.outputs.has_changes == 'true'
uses: actions/github-script@v7
env:
DUP_OUTCOME: ${{ steps.duplicates.outcome }}
VAL_OUTCOME: ${{ steps.validation.outcome }}
BUILD_OUTCOME: ${{ steps.build.outcome }}
DUP_RESULT: ${{ steps.duplicates.outputs.result }}
VAL_RESULT: ${{ steps.validation.outputs.result }}
BUILD_RESULT: ${{ steps.build.outputs.result }}
with:
script: |
const dup = `${{ steps.duplicates.outcome }}`;
const val = `${{ steps.validation.outcome }}`;
const build = `${{ steps.build.outcome }}`;
const fs = require('fs');

const dupResult = `${{ steps.duplicates.outputs.result }}`;
const valResult = `${{ steps.validation.outputs.result }}`;
const buildResult = `${{ steps.build.outputs.result }}`;
const dup = process.env.DUP_OUTCOME;
const val = process.env.VAL_OUTCOME;
const build = process.env.BUILD_OUTCOME;

// The shell steps emit literal "\n" sequences; turn them into real
// newlines so the error list renders as Markdown bullets.
const fixup = (s) => (s || '').replace(/\\n/g, '\n');
const dupResult = fixup(process.env.DUP_RESULT);
const valResult = fixup(process.env.VAL_RESULT);
const buildResult = fixup(process.env.BUILD_RESULT);

const allPassed = dup === 'success' && val === 'success' && build === 'success';
const icon = (s) => s === 'success' ? ':white_check_mark:' : s === 'skipped' ? ':next_track_button:' : ':x:';
Expand All @@ -259,28 +280,14 @@ jobs:
body += `**HTML:**\n\`\`\`html\n<a href="https://agenthub.nullorder.org">\n <img src="https://agenthub.nullorder.org/badge.svg" alt="AgentHub" />\n</a>\n\`\`\`\n`;
}

// Find and update existing bot comment, or create new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const marker = '## Plugin Validation';
const existing = comments.find(c => c.body.startsWith(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: context.issue.number,
body,
});
}
fs.mkdirSync('pr-comment', { recursive: true });
fs.writeFileSync('pr-comment/body.md', body);
fs.writeFileSync('pr-comment/pr.txt', String(context.issue.number));

- name: Upload PR comment artifact
if: always() && steps.changes.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: pr-comment
path: pr-comment/
retention-days: 1