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
57 changes: 34 additions & 23 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,10 @@ runs:
annotations_source="$json_file"
rating_source="$json_file"
if [ "${MESH_DIFF:-false}" = "true" ] && [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ]; then
base_sha="$(jq -r '.pull_request.base.sha // empty' "$GITHUB_EVENT_PATH")"
# Read the PR base SHA from the event payload with Node (always
# present — this is a Node action) rather than jq, which is not
# guaranteed on self-hosted or minimal runners.
base_sha="$(node -e 'try{const fs=require("node:fs");const e=JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH,"utf8"));process.stdout.write(e.pull_request?.base?.sha ?? "")}catch{process.stdout.write("")}')"
if [ -n "$base_sha" ]; then
git fetch --no-tags --depth=1 origin "$base_sha" 2>/dev/null || true
base_dir="${RUNNER_TEMP:-.}/policymesh-base-worktree"
Expand Down Expand Up @@ -145,29 +148,37 @@ runs:
# pull_request event. Uses an HTML marker to find-and-update the
# existing comment in place rather than spam a new one per push.
if [ -n "${MESH_GITHUB_TOKEN:-}" ] && [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ]; then
pr_number="$(jq -r '.pull_request.number // empty' "$GITHUB_EVENT_PATH")"
if [ -n "$pr_number" ]; then
marker='<!-- policymesh:pr-summary -->'
comment_file="${RUNNER_TEMP:-.}/policymesh-comment.md"
{
printf '%s\n\n' "$marker"
cat "$report_file"
} > "$comment_file"

existing="$(GH_TOKEN="$MESH_GITHUB_TOKEN" gh api \
"repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" \
--paginate \
--jq ".[] | select(.body != null and (.body | contains(\"$marker\"))) | .id" \
| head -n 1)"

if [ -n "$existing" ]; then
GH_TOKEN="$MESH_GITHUB_TOKEN" gh api -X PATCH \
"repos/${GITHUB_REPOSITORY}/issues/comments/${existing}" \
-F body=@"$comment_file" > /dev/null
else
GH_TOKEN="$MESH_GITHUB_TOKEN" gh api -X POST \
if ! command -v gh >/dev/null 2>&1; then
# gh is preinstalled on GitHub-hosted runners but may be absent
# on self-hosted or minimal images. Degrade gracefully rather
# than failing the step — the Markdown report is already in the
# step summary.
echo "::notice::PolicyMesh: sticky PR comment skipped because the GitHub CLI (gh) is not available on this runner. The Markdown report is still in the step summary."
else
pr_number="$(node -e 'try{const fs=require("node:fs");const e=JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH,"utf8"));process.stdout.write(String(e.pull_request?.number ?? ""))}catch{process.stdout.write("")}')"
if [ -n "$pr_number" ]; then
marker='<!-- policymesh:pr-summary -->'
comment_file="${RUNNER_TEMP:-.}/policymesh-comment.md"
{
printf '%s\n\n' "$marker"
cat "$report_file"
} > "$comment_file"

existing="$(GH_TOKEN="$MESH_GITHUB_TOKEN" gh api \
"repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" \
-F body=@"$comment_file" > /dev/null
--paginate \
--jq ".[] | select(.body != null and (.body | contains(\"$marker\"))) | .id" \
| head -n 1)"

if [ -n "$existing" ]; then
GH_TOKEN="$MESH_GITHUB_TOKEN" gh api -X PATCH \
"repos/${GITHUB_REPOSITORY}/issues/comments/${existing}" \
-F body=@"$comment_file" > /dev/null
else
GH_TOKEN="$MESH_GITHUB_TOKEN" gh api -X POST \
"repos/${GITHUB_REPOSITORY}/issues/${pr_number}/comments" \
-F body=@"$comment_file" > /dev/null
fi
fi
fi
fi
Expand Down
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@
"@types/node": "^24.0.0",
"typescript": "^5.9.3"
},
"engines": {
"node": ">=20"
},
"license": "MIT"
}
22 changes: 22 additions & 0 deletions test/workflow.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
Expand Down Expand Up @@ -153,6 +153,28 @@
assert.ok(trackedDistFiles.includes('dist/audit.js'));
});

test('action.yml reads the PR event with Node and guards the GitHub CLI', async () => {
const action = await readFile(join(packageRoot, 'action.yml'), 'utf8');

// No dependency on the standalone jq binary for parsing the event payload
// — minimal / self-hosted runners may not have it.
assert.doesNotMatch(action, /jq -r '\.pull_request/);
// PR base SHA + number are read with Node straight from the event JSON.
assert.match(action, /process\.env\.GITHUB_EVENT_PATH/);
assert.match(action, /pull_request\?\.base\?\.sha/);
// gh is guarded so a runner without it degrades to a notice, not a failure.
assert.match(action, /command -v gh/);
});

test('package.json and lockfile declare a supported Node engine', async () => {
const packageJson = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
const packageLock = JSON.parse(await readFile(join(packageRoot, 'package-lock.json'), 'utf8'));

assert.deepEqual(packageJson.engines, { node: '>=20' });
// Keep the lockfile's root entry in sync so `npm ci` does not warn on drift.
assert.deepEqual(packageLock.packages[''].engines, { node: '>=20' });
});

test('CI workflow builds and tests PolicyMesh', async () => {
const workflow = await readFile(join(packageRoot, '.github', 'workflows', 'ci.yml'), 'utf8');

Expand Down