1+ on :
2+ pull_request_target :
3+ types : [opened, reopened, synchronize]
4+ issue_comment :
5+ types : [created]
6+
7+ permissions :
8+ contents : read # needed for checkout
9+ pull-requests : write # needed for commenting & reviews (gh) when running in pull_request_target
10+ checks : write # needed to create check runs
11+
12+ jobs :
13+ # Job that posts the questionnaire (runs in the trusted pull_request_target context).
14+ post-questionnaire :
15+ if : github.event_name == 'pull_request_target' && github.event.pull_request.draft == false
16+ runs-on : ubuntu-latest
17+ steps :
18+ - name : Checkout base repo (safe; do NOT checkout PR head here)
19+ uses : actions/checkout@v4
20+ with :
21+ ref : ${{ github.event.pull_request.base.sha }}
22+ fetch-depth : 0
23+
24+ - name : Authenticate gh CLI with GITHUB_TOKEN
25+ run : |
26+ echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
27+
28+ - name : Check if questionnaire already answered
29+ id : check
30+ run : |
31+ PR_NUMBER=${{ github.event.pull_request.number }}
32+ # Collect PR comments (robust to empty output)
33+ RESPONSES=$(gh pr view "$PR_NUMBER" --json comments --jq '.comments[].body' 2>/dev/null | grep -i "Ethics & Regulatory Questionnaire" -A 20 || true)
34+ if [[ -z "$RESPONSES" ]]; then
35+ echo "status=missing" >> $GITHUB_OUTPUT
36+ else
37+ echo "status=answered" >> $GITHUB_OUTPUT
38+ fi
39+
40+ - name : Post questionnaire if missing
41+ if : steps.check.outputs.status == 'missing'
42+ run : |
43+ # ensure file exists in the base repo checkout (case-sensitive)
44+ if [[ ! -f .github/ETHICS_QUESTIONNAIRE.MD ]]; then
45+ echo ".github/ETHICS_QUESTIONNAIRE.MD not found in base repo; aborting." >&2
46+ exit 1
47+ fi
48+ gh pr comment ${{ github.event.pull_request.number }} --body-file .github/ETHICS_QUESTIONNAIRE.MD
49+ echo "Posted ethics questionnaire to PR #${{ github.event.pull_request.number }}."
50+
51+ # Ethics engine: collects comments, runs evaluation, posts a check, and requests changes for HIGH risk.
52+ # This job runs in the trusted context for pull_request_target and also on issue_comment (untrusted).
53+ # For untrusted issue_comment runs, write actions (requesting changes) may be skipped if permissions are restricted.
54+ ethics-engine :
55+ runs-on : ubuntu-latest
56+ needs : post-questionnaire
57+ steps :
58+ - name : Checkout base repo (we run parser from base repo)
59+ uses : actions/checkout@v4
60+ with :
61+ ref : ${{ github.event.pull_request.base.sha || github.ref }}
62+ fetch-depth : 0
63+
64+ - name : Authenticate gh CLI with GITHUB_TOKEN
65+ run : |
66+ echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
67+
68+ - name : Determine PR number
69+ id : prnumber
70+ run : |
71+ # Determine PR number whether triggered by pull_request_target or issue_comment
72+ PR_NUMBER=$(jq -r 'if .pull_request then .pull_request.number elif .issue then .issue.number else empty end' "$GITHUB_EVENT_PATH")
73+ if [[ -z "$PR_NUMBER" ]]; then
74+ echo "No PR number found in event payload; exiting."
75+ echo "risk=UNKNOWN" >> $GITHUB_OUTPUT
76+ exit 0
77+ fi
78+ echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
79+
80+ - name : Collect comments
81+ id : collect
82+ run : |
83+ PR=${{ steps.prnumber.outputs.pr_number }}
84+ # Gather all PR comments into a single string (robust to empty)
85+ ANSWERS=$(gh pr view "$PR" --json comments --jq '[.comments[].body] | join("\n\n")' 2>/dev/null || true)
86+ echo "$ANSWERS" > answers.txt
87+ # Expose the answers (trim to avoid huge output)
88+ echo "answers=$(echo "$ANSWERS" | head -c 32768 | sed -e 's/"/'"'"'"/g')" >> $GITHUB_OUTPUT
89+
90+ - name : Run ethics parser & evaluator (safe runs code from base repo)
91+ id : run_engine
92+ env :
93+ PR_NUMBER : ${{ steps.prnumber.outputs.pr_number }}
94+ run : |
95+ # Ensure parser exists
96+ if [[ ! -f .github/workflows/parse_and_evaluate.py ]]; then
97+ echo "Parser .github/workflows/parse_and_evaluate.py not found in base repo; aborting."
98+ echo "RISK_LEVEL=UNKNOWN" > result.txt
99+ else
100+ python3 .github/workflows/parse_and_evaluate.py "$(cat answers.txt)" > result.txt || true
101+ fi
102+ cat result.txt
103+ # Extract RISK_LEVEL=XYZ from result.txt if present
104+ RISK=$(grep -m1 '^RISK_LEVEL=' result.txt | cut -d= -f2 || echo "LOW")
105+ echo "risk=$RISK" >> $GITHUB_OUTPUT
106+
107+ - name : Create/update "Ethics Review" check run
108+ uses : actions/github-script@v7
109+ with :
110+ github-token : ${{ secrets.GITHUB_TOKEN }}
111+ script : |
112+ const risk = "${{ steps.run_engine.outputs.risk }}".trim();
113+ const conclusions = {
114+ "LOW": "success",
115+ "MEDIUM": "action_required",
116+ "HIGH": "failure"
117+ };
118+ const conclusion = conclusions[risk] || "failure";
119+ const head_sha = (context.payload.pull_request && context.payload.pull_request.head && context.payload.pull_request.head.sha) || (context.payload.issue && context.payload.issue.pull_request && context.payload.issue.number ? undefined : undefined) || github.event.pull_request?.head?.sha;
120+ await github.rest.checks.create({
121+ owner: context.repo.owner,
122+ repo: context.repo.repo,
123+ name: "Ethics Review",
124+ head_sha: head_sha || context.sha,
125+ status: "completed",
126+ conclusion,
127+ output: {
128+ title: risk === "LOW" ? "Ethics cleared" : `Ethics review: ${risk}`,
129+ summary: risk === "LOW" ? "Low risk – automatically approved" : `Risk level ${risk} – review required`
130+ }
131+ });
132+
133+ - name : Request changes on HIGH risk (trusted-only; skip on untrusted events)
134+ if : steps.run_engine.outputs.risk == 'HIGH'
135+ run : |
136+ PR=${{ steps.prnumber.outputs.pr_number }}
137+ # Only attempt to request changes when running in pull_request_target context (trusted).
138+ if [[ "${GITHUB_EVENT_NAME}" != "pull_request_target" ]]; then
139+ echo "Not in pull_request_target context; skipping request-changes (insufficient permissions for fork PRs)."
140+ exit 0
141+ fi
142+ # Request changes using gh (GITHUB_TOKEN from pull_request_target has write rights)
143+ gh pr review "$PR" --request-changes -b "@ethics-team Required manual review for high-risk change"
144+ echo "Requested changes on PR #$PR due to HIGH risk."
145+
146+ - name : Final status message
147+ run : |
148+ echo "Ethics engine completed. Risk level: ${{ steps.run_engine.outputs.risk }}"
0 commit comments