Skip to content

Commit a80fb89

Browse files
committed
[ core/pre-review ] DRAFT: github-actions pre-review.
Sets workflow to run a script checking common PR issues. - Provides a feedbacks message based on contribution - Sets label according to the feedback or maintainer actions
1 parent d6cc6f9 commit a80fb89

6 files changed

Lines changed: 305 additions & 0 deletions

File tree

.github/workflows/pre-review.yml

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
name: PR Pre-review
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- master
7+
8+
env:
9+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10+
SKIP_REST: false
11+
12+
jobs:
13+
pre-review:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
18+
- uses: actions/checkout@v4
19+
with:
20+
ref: ${{ github.head_ref }}
21+
fetch-depth: 0
22+
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: 20
26+
cache: 'npm'
27+
28+
- name: Fetch master branch
29+
run: git fetch origin master
30+
31+
- name: Collect information
32+
run: |
33+
setEnv() {
34+
echo "$1=${!1}" >> $GITHUB_ENV
35+
}
36+
37+
MERGE_BASE=$(git merge-base origin/master HEAD 2>/dev/null || echo HEAD~1)
38+
GITHUB_HANDLE=${GITHUB_TRIGGERING_ACTOR:-local-actor}; setEnv "GITHUB_HANDLE"
39+
CHANGED_FILES=$(git diff --name-only $MERGE_BASE HEAD | tr '\n' ' '); setEnv "CHANGED_FILES";
40+
GITHUB_PERMISSION_ROLE=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/${{ github.actor }}/permission" -q '.role_name' 2>/dev/null || echo 'unknown');
41+
setEnv "GITHUB_PERMISSION_ROLE"
42+
43+
- name: Run "preReview" script
44+
run: |
45+
REVIEW_MESSAGE=$(node ./generators/preReview.js "$GITHUB_HANDLE" "$CHANGED_FILES" "$GITHUB_PERMISSION_ROLE")
46+
echo "REVIEW_MESSAGE=$REVIEW_MESSAGE" >> $GITHUB_ENV
47+
gh pr review "$PR_NUMBER" --request-changes --body "$REVIEW_MESSAGE"
48+
49+
- name: Assign labels based on feedback message
50+
run: |
51+
# "Changes Requested" label cleanup or added
52+
CURRENT_LABELS=$(gh pr view "$PR_NUMBER" --json labels -q '.labels[].name')
53+
54+
if [[ "$REVIEW_MESSAGE" == *"- [ ]"* ]]; then
55+
gh pr edit "$PR_NUMBER" --add-label "Changes requested"
56+
else
57+
gh pr edit "$PR_NUMBER" --remove-label "Changes Requested"
58+
fi
59+
60+
# "Invalid" label cleanup or added
61+
if [[ "$REVIEW_MESSAGE" == *"Missing"* ]]; then
62+
gh pr edit "$PR_NUMBER" --add-label "Invalid"
63+
else
64+
gh pr edit "$PR_NUMBER" --remove-label "Invalid"
65+
fi
66+
67+
# maintainer-review:
68+
# if: github.event_name == 'pull_request_review'
69+
# runs-on: ubuntu-latest
70+
71+
# steps:
72+
# - name: Add labels depending on maintainer review
73+
# run: |
74+
# PR_NUMBER=${{ github.event.pull_request.number }}
75+
# REVIEW_STATE=${{ github.event.review.state }}
76+
# CURRENT_MONTH=$(date +%m)
77+
78+
# if [[ "$REVIEW_STATE" == "changes_requested" ]]; then
79+
# gh pr edit "$PR_NUMBER" --add-label "Changes Requested"
80+
# echo "Maintainer requested changes — label added."
81+
# elif [[ "$REVIEW_STATE" == "approved" ]]; then
82+
# gh pr edit "$PR_NUMBER" --add-label "Approved"
83+
# echo "Maintainer approved — label added."
84+
# fi
85+
86+
# # Add Hacktoberfest label only if current month is October (10)
87+
# if [[ "$CURRENT_MONTH" == "10" ]]; then
88+
# gh pr edit "$PR_NUMBER" --add-label "hacktoberfest-accepted"
89+
# fi
90+
91+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plop
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
coucou
2+
<script>console.log("hello you how are you ")</script>

Art/LaurelineP/test-workflow/meta.json

Whitespace-only changes.

Art/LaurelineP/test-workflow/styles.css

Whitespace-only changes.

generators/preReview.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
const fs = require("node:fs/promises")
2+
3+
/* Github Actions values to use */
4+
const contributorHandle = process.env.GITHUB_HANDLE || ''
5+
const changedFilesStr = process.env?.CHANGED_FILES || ''
6+
const canChangeAnyFiles = [ 'admin', 'maintain' ].includes(process.env?.GITHUB_PERMISSION_ROLE || '')
7+
8+
9+
/* Helpers */
10+
const createFeedbacks = (feedbacks = [], mode = 'line') => {
11+
return (message) => {
12+
const feedback = mode == 'task' ? `- [ ] ${message}` : message
13+
feedbacks.push(feedback)
14+
}
15+
}
16+
17+
/* ------------------------------------ - ----------------------------------- */
18+
19+
/** Establishes a state about the contribution */
20+
const getContributionState = () => {
21+
const changedFiles = changedFilesStr.split(" ") || []
22+
const requiredRegexp = /(index.html|styles.css|meta.json)$/
23+
const fileCorrectness = changedFiles.reduce((details, file) => {
24+
25+
// Separates changed file to "incorrectFiles" or "correctFiles"
26+
const detailsProperty = requiredRegexp.test( file )? details.correctFiles : details.incorrectFiles
27+
detailsProperty.push( file )
28+
return details
29+
}, { incorrectFiles: [], correctFiles: []})
30+
31+
const state = {
32+
changedFiles,
33+
...fileCorrectness
34+
}
35+
return state
36+
}
37+
38+
39+
40+
/** Review HTML file - returning an array of feedbacks */
41+
const reviewHTMLFile = (file, fileContent) => {
42+
const feedbacks = []
43+
if (file.includes('.html')){
44+
const feedbackPush = createFeedbacks(feedbacks, 'task')
45+
46+
const isHTMLWithJS = /<(.+)?script(.+)?>/.test(fileContent)
47+
const hasValidStylesheet = /<(.+)?link(.+)?href=(\.+)?styles.css>/gi.test(fileContent)
48+
console.log('hasValidStylesheet:', hasValidStylesheet)
49+
if ( isHTMLWithJS ){
50+
feedbackPush('Remove the JavaScript contained in your HTML file: JavaScript is not allowed')
51+
}
52+
if ( !hasValidStylesheet ){
53+
const isMissingCSS = !hasValidStylesheet && !/\w.css/gi.test(fileContent)
54+
const message = isMissingCSS
55+
? 'Missing linked stylesheet file: link the CSS file to your HTML'
56+
: 'Remove the JavaScript contained in your HTML file: JavaScript is not allowed'
57+
58+
feedbackPush(message)
59+
}
60+
61+
if(feedbacks.length) feedbacks.unshift('### HTML feedbacks')
62+
return feedbacks
63+
}
64+
65+
}
66+
67+
/** Review CSS file - returning an array of feedbacks */
68+
const reviewCSSFile = (file, fileContent) => {
69+
const feedbacks = []
70+
if (!file.includes('.css')) return;
71+
const feedbackPush = createFeedbacks(feedbacks, 'task')
72+
73+
// CSS animation(s) check
74+
const hasCSSAnimation = /@keyframes/gi.test(fileContent)
75+
if( !fileContent ){
76+
feedbackPush(`Missing content, add the code for the file \`${file}\``)
77+
78+
} else if( !hasCSSAnimation ){
79+
feedbackPush('Add animation(s) using the "@keyframes" in your CSS')
80+
}
81+
82+
83+
84+
if(feedbacks.length) feedbacks.unshift('### CSS feedbacks')
85+
return feedbacks
86+
}
87+
88+
/** Review JSON file - returning an array of feedbacks */
89+
const reviewJSONFile = (file, fileContent) => {
90+
const feedbacks = []
91+
if (!file.includes('.json')) return;
92+
93+
const feedbackPush = createFeedbacks(feedbacks, 'task')
94+
95+
// Meta checks
96+
const contributorRegexp = new RegExp(contributorHandle, 'i')
97+
const hasMetaArtNameValue = /artName\s?:\s?\w+./.test(fileContent)
98+
const hasMetaGithubHandleValue = /githubHandle\s?:\?\w+./.test(fileContent)
99+
const hasCorrectMetaGithubHandle = contributorRegexp.test(fileContent)
100+
101+
102+
!hasMetaArtNameValue && feedbackPush('Missing artName: add an "artName"')
103+
if( hasMetaGithubHandleValue && !hasCorrectMetaGithubHandle ){
104+
feedbackPush('Unmatched github handler: adjust your `githubHandle`')
105+
} else if ( !hasMetaGithubHandleValue ){
106+
feedbackPush('Missing github handle: add your github handle in `githubHandle`')
107+
}
108+
109+
if(feedbacks.length) feedbacks.unshift('### JSON feedbacks')
110+
return feedbacks
111+
}
112+
113+
114+
const reviewContribution = (contributionStates) => {
115+
const feedbacks = []
116+
const feedbackPush = createFeedbacks(feedbacks, 'task')
117+
118+
const { correctFiles, incorrectFiles } = contributionStates
119+
const allFiles = [...correctFiles, ...incorrectFiles ]
120+
121+
// Unecessary files
122+
const folderRegexp = new RegExp(`^Art/${contributorHandle}`, 'i')
123+
124+
// Handle incorrect files
125+
for (const file of incorrectFiles){
126+
const hasValidFolderName = folderRegexp.test(file)
127+
128+
if(!canChangeAnyFiles){
129+
// File(s) outside of the Art folder
130+
if(!hasValidFolderName){
131+
feedbackPush(`Remove unnecessary file: \`${file}\``)
132+
} else { // Incorrect file names in Art contribution folder
133+
feedbackPush(`Rename your file as recommended: \`${file}\``)
134+
}
135+
} else {
136+
console.log("Skipping file validation for maintainer/admin role.")
137+
}
138+
139+
}
140+
141+
if(feedbacks.length) feedbacks.unshift('### Other feedbacks')
142+
return feedbacks
143+
144+
}
145+
146+
const checkContent = async (contributionStates) => {
147+
let feedbacks = []
148+
const feedbackPush = createFeedbacks(feedbacks, 'task')
149+
150+
const { incorrectFiles, correctFiles } = contributionStates
151+
const changedFiles = [ ...incorrectFiles, ...correctFiles ]
152+
let HTMLReviews = [], CSSReviews = [], JSONReviews = []
153+
154+
// Checks
155+
for( const file of changedFiles ){
156+
let fileContent = ''
157+
try {
158+
fileContent = await fs.readFile(file, 'utf-8')
159+
} catch {
160+
feedbackPush(`File not found or unreadable: \`${file}\``)
161+
continue
162+
}
163+
164+
if(!fileContent){
165+
feedbackPush(`Missing content, add the code for the file \`${file}\``)
166+
} else {
167+
HTMLReviews = reviewHTMLFile(file, fileContent) || HTMLReviews
168+
CSSReviews = reviewCSSFile(file, fileContent) || CSSReviews
169+
JSONReviews = reviewJSONFile(file, fileContent) || JSONReviews
170+
}
171+
}
172+
feedbacks = [
173+
...feedbacks,
174+
...HTMLReviews,
175+
...CSSReviews,
176+
...JSONReviews,
177+
]
178+
const otherReviews = reviewContribution(contributionStates)
179+
return ([
180+
...feedbacks,
181+
...otherReviews
182+
])
183+
}
184+
185+
const generateReviewMessage = (feedbacks ) => {
186+
const messageLines = [`Aloha @${contributorHandle} 🙌 Thanks for your contribution!`]
187+
const messagePush = createFeedbacks(messageLines, 'line')
188+
189+
190+
if( feedbacks.length ) {
191+
messagePush("## Feedbacks")
192+
messagePush("Before we could merge, please address the following:")
193+
194+
const taskList = feedbacks.join("\n")
195+
messagePush(`${ taskList }`)
196+
} else {
197+
messagePush("Seems to meet requirements, now awaiting for a maintener last validation.")
198+
}
199+
200+
201+
messageLines.push( "Happy Coding! 🚀")
202+
const messageReview = messageLines.join("\n\n")
203+
return messageReview
204+
}
205+
206+
;(async () => {
207+
const contributionState = getContributionState()
208+
const feedbacks = await checkContent(contributionState)
209+
const PRFinalReview = generateReviewMessage(feedbacks)
210+
console.info(`\n\n${PRFinalReview}\n\n`)
211+
})()

0 commit comments

Comments
 (0)