Skip to content

Commit ad42264

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 WIP: reviewing gh actions
1 parent d6cc6f9 commit ad42264

6 files changed

Lines changed: 288 additions & 0 deletions

File tree

.github/workflows/pre-review.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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_HANDLE/permission" -q '.role_name' 2>/dev/null || echo 'unknown');
41+
setEnv "GITHUB_PERMISSION_ROLE"
42+
43+
- name: Run "preReview" script
44+
run: |
45+
PR_NUMBER=${{ github.event.pull_request.number }};
46+
REVIEW_MESSAGE=$(node ./generators/preReview.js "$GITHUB_HANDLE" "$CHANGED_FILES" "$GITHUB_PERMISSION_ROLE");
47+
echo "REVIEW_MESSAGE<<EOF" >> $GITHUB_ENV
48+
echo "$REVIEW_MESSAGE" >> $GITHUB_ENV
49+
echo "EOF" >> $GITHUB_ENV
50+
# Show in logs
51+
echo "===== REVIEW_MESSAGE ====="
52+
echo "$REVIEW_MESSAGE"
53+
echo "=========================="
54+
55+
maintainer-review:
56+
if: github.event_name == 'pull_request_review'
57+
runs-on: ubuntu-latest
58+
59+
steps:
60+
- name: Add labels depending on maintainer review
61+
run: |
62+
PR_NUMBER=${{ github.event.pull_request.number }}
63+
REVIEW_STATE=${{ github.event.review.state }}
64+
CURRENT_MONTH=$(date +%m)
65+
66+
if [[ "$REVIEW_STATE" == "changes_requested" ]]; then
67+
gh pr edit "$PR_NUMBER" --add-label "Changes Requested"
68+
echo "Maintainer requested changes — label added."
69+
elif [[ "$REVIEW_STATE" == "approved" ]]; then
70+
gh pr edit "$PR_NUMBER" --add-label "Approved"
71+
echo "Maintainer approved — label added."
72+
fi
73+
74+
# Add Hacktoberfest label only if current month is October (10)
75+
if [[ "$CURRENT_MONTH" == "10" ]]; then
76+
gh pr edit "$PR_NUMBER" --add-label "hacktoberfest-accepted"
77+
fi
78+
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: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
if ( isHTMLWithJS ){
49+
feedbackPush('Remove the JavaScript contained in your HTML file: JavaScript is not allowed')
50+
}
51+
if ( !hasValidStylesheet ){
52+
const isMissingCSS = !hasValidStylesheet && !/\w.css/gi.test(fileContent)
53+
const message = isMissingCSS
54+
? 'Missing linked stylesheet file: link the CSS file to your HTML'
55+
: 'Remove the JavaScript contained in your HTML file: JavaScript is not allowed'
56+
57+
feedbackPush(message)
58+
}
59+
60+
if(feedbacks.length) feedbacks.unshift('### HTML feedbacks')
61+
return feedbacks
62+
}
63+
64+
}
65+
66+
/** Review CSS file - returning an array of feedbacks */
67+
const reviewCSSFile = (file, fileContent) => {
68+
const feedbacks = []
69+
if (!file.includes('.css')) return;
70+
const feedbackPush = createFeedbacks(feedbacks, 'task')
71+
72+
// CSS animation(s) check
73+
const hasCSSAnimation = /@keyframes/gi.test(fileContent)
74+
if( !fileContent ){
75+
feedbackPush(`Missing content, add the code for the file \`${file}\``)
76+
77+
} else if( !hasCSSAnimation ){
78+
feedbackPush('Add animation(s) using the "@keyframes" in your CSS')
79+
}
80+
81+
82+
83+
if(feedbacks.length) feedbacks.unshift('### CSS feedbacks')
84+
return feedbacks
85+
}
86+
87+
/** Review JSON file - returning an array of feedbacks */
88+
const reviewJSONFile = (file, fileContent) => {
89+
const feedbacks = []
90+
if (!file.includes('.json')) return;
91+
92+
const feedbackPush = createFeedbacks(feedbacks, 'task')
93+
94+
// Meta checks
95+
const contributorRegexp = new RegExp(contributorHandle, 'i')
96+
const hasMetaArtNameValue = /artName\s?:\s?\w+./.test(fileContent)
97+
const hasMetaGithubHandleValue = /githubHandle\s?:\?\w+./.test(fileContent)
98+
const hasCorrectMetaGithubHandle = contributorRegexp.test(fileContent)
99+
100+
101+
!hasMetaArtNameValue && feedbackPush('Missing artName: add an "artName"')
102+
if( hasMetaGithubHandleValue && !hasCorrectMetaGithubHandle ){
103+
feedbackPush('Unmatched github handler: adjust your `githubHandle`')
104+
} else if ( !hasMetaGithubHandleValue ){
105+
feedbackPush('Missing github handle: add your github handle in `githubHandle`')
106+
}
107+
108+
if(feedbacks.length) feedbacks.unshift('### JSON feedbacks')
109+
return feedbacks
110+
}
111+
112+
113+
const reviewContribution = (contributionStates) => {
114+
const feedbacks = []
115+
const feedbackPush = createFeedbacks(feedbacks, 'task')
116+
117+
const { correctFiles, incorrectFiles } = contributionStates
118+
const allFiles = [...correctFiles, ...incorrectFiles ]
119+
120+
// Unecessary files
121+
const folderRegexp = new RegExp(`^Art/${contributorHandle}`, 'i')
122+
123+
// Handle incorrect files
124+
for (const file of incorrectFiles){
125+
const hasValidFolderName = folderRegexp.test(file)
126+
127+
if(!canChangeAnyFiles){
128+
// File(s) outside of the Art folder
129+
if(!hasValidFolderName){
130+
feedbackPush(`Remove unnecessary file: \`${file}\``)
131+
} else { // Incorrect file names in Art contribution folder
132+
feedbackPush(`Rename your file as recommended: \`${file}\``)
133+
}
134+
}
135+
}
136+
137+
if(feedbacks.length) feedbacks.unshift('### Other feedbacks')
138+
return feedbacks
139+
140+
}
141+
142+
const checkContent = async (contributionStates) => {
143+
let feedbacks = []
144+
const feedbackPush = createFeedbacks(feedbacks, 'task')
145+
146+
const { incorrectFiles, correctFiles } = contributionStates
147+
const changedFiles = [ ...incorrectFiles, ...correctFiles ]
148+
let HTMLReviews = [], CSSReviews = [], JSONReviews = []
149+
150+
// Checks
151+
for( const file of changedFiles ){
152+
let fileContent = ''
153+
try {
154+
fileContent = await fs.readFile(file, 'utf-8')
155+
} catch {
156+
feedbackPush(`File not found or unreadable: \`${file}\``)
157+
continue
158+
}
159+
160+
if(!fileContent){
161+
feedbackPush(`Missing content, add the code for the file \`${file}\``)
162+
} else {
163+
HTMLReviews = reviewHTMLFile(file, fileContent) || HTMLReviews
164+
CSSReviews = reviewCSSFile(file, fileContent) || CSSReviews
165+
JSONReviews = reviewJSONFile(file, fileContent) || JSONReviews
166+
}
167+
}
168+
feedbacks = [
169+
...feedbacks,
170+
...HTMLReviews,
171+
...CSSReviews,
172+
...JSONReviews,
173+
]
174+
const otherReviews = reviewContribution(contributionStates)
175+
return ([
176+
...feedbacks,
177+
...otherReviews
178+
])
179+
}
180+
181+
const generateReviewMessage = (feedbacks ) => {
182+
const messageLines = [`Aloha @${contributorHandle} 🙌 Thanks for your contribution!`]
183+
const messagePush = createFeedbacks(messageLines, 'line')
184+
185+
186+
if( feedbacks.length ) {
187+
messagePush("Before we could merge, please address the following:")
188+
messagePush("## Feedbacks")
189+
190+
const taskList = feedbacks.join("\n")
191+
messagePush(`${ taskList }`)
192+
} else {
193+
messagePush("Seems to meet requirements, now awaiting for a maintener last validation.")
194+
}
195+
196+
197+
messageLines.push( "Happy Coding! 🚀")
198+
const messageReview = messageLines.join("\n")
199+
return messageReview
200+
}
201+
202+
;(async () => {
203+
const contributionState = getContributionState()
204+
const feedbacks = await checkContent(contributionState)
205+
const PRFinalReview = generateReviewMessage(feedbacks)
206+
console.info(`\n\n${PRFinalReview}\n\n`)
207+
})()

0 commit comments

Comments
 (0)