Skip to content

Commit be93442

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 - Adding label
1 parent d6cc6f9 commit be93442

6 files changed

Lines changed: 314 additions & 0 deletions

File tree

.github/workflows/pre-review.yml

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
- uses: actions/checkout@v4
18+
with:
19+
ref: ${{ github.head_ref }}
20+
fetch-depth: 0
21+
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: 20
25+
cache: 'npm'
26+
27+
- name: Fetch master branch
28+
run: git fetch origin master
29+
30+
- name: Collect information
31+
run: |
32+
# Environment variables used with "preReview.js"
33+
echo "Setting environment variable for \"preReview.js\""
34+
setEnv() {
35+
echo "$1=${!1}" >> $GITHUB_ENV
36+
}
37+
38+
MERGE_BASE=$(git merge-base origin/master HEAD 2>/dev/null || echo HEAD~1)
39+
GITHUB_HANDLE=${GITHUB_TRIGGERING_ACTOR:-local-actor}; setEnv "GITHUB_HANDLE"
40+
CHANGED_FILES=$(git diff --name-only $MERGE_BASE HEAD | tr '\n' ' '); setEnv "CHANGED_FILES";
41+
GITHUB_PERMISSION_ROLE=$(gh api "repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_HANDLE/permission" -q '.role_name' 2>/dev/null || echo 'unknown');
42+
setEnv "GITHUB_PERMISSION_ROLE"
43+
44+
- name: Run "preReview" script
45+
id: pre-review
46+
run: |
47+
PR_NUMBER=${{ github.event.pull_request.number }};
48+
REVIEW_MESSAGE=$(node ./generators/preReview.js "$GITHUB_HANDLE" "$CHANGED_FILES" "$GITHUB_PERMISSION_ROLE");
49+
echo "REVIEW_MESSAGE<<EOF" >> $GITHUB_ENV
50+
echo "$REVIEW_MESSAGE" >> $GITHUB_ENV
51+
echo "EOF" >> $GITHUB_ENV
52+
53+
# Show in logs
54+
echo "===== REVIEW_MESSAGE ====="
55+
echo "$REVIEW_MESSAGE"
56+
echo "=========================="
57+
58+
# Submit review
59+
echo "Submit review comment"
60+
gh pr comment "$PR_NUMBER" --body "$REVIEW_MESSAGE"
61+
62+
# Add label
63+
echo "Submit review comment"
64+
if echo "$REVIEW_MESSAGE" | grep -q '\- \[ \]'; then
65+
gh pr edit "$PR_NUMBER" --add-assignee "$GITHUB_HANDLE"
66+
gh pr edit "$PR_NUMBER" --add-label "Changes Requested"
67+
else
68+
gh pr edit "$PR_NUMBER" --remove-label "Changes Requested"
69+
fi
70+
if echo "$REVIEW_MESSAGE" | grep -q -Eqi 'missing|invalid'; then
71+
gh pr edit "$PR_NUMBER" --add-label "Invalid"
72+
else
73+
gh pr edit "$PR_NUMBER" --remove-label "Invalid"
74+
fi
75+
76+
env:
77+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78+
79+
maintainer-review:
80+
needs: pre-review
81+
if: github.event_name == 'pull_request_review'
82+
runs-on: ubuntu-latest
83+
84+
steps:
85+
86+
- name: Show previous review message
87+
run: |
88+
echo "Message from pre-review job:"
89+
echo "${{ needs.pre-review.outputs.review_message }}"
90+
91+
- name: Add labels depending on maintainer review
92+
run: |
93+
PR_NUMBER=${{ github.event.pull_request.number }}
94+
REVIEW_STATE=${{ github.event.review.state }}
95+
CURRENT_MONTH=$(date +%m)
96+
97+
if [[ "$REVIEW_STATE" == "changes_requested" ]]; then
98+
gh pr edit "$PR_NUMBER" --add-label "Changes Requested"
99+
echo "Maintainer requested changes — label added."
100+
elif [[ "$REVIEW_STATE" == "approved" ]]; then
101+
gh pr edit "$PR_NUMBER" --add-label "Approved"
102+
echo "Maintainer approved — label added."
103+
fi
104+
105+
# Add Hacktoberfest label only if current month is October (10)
106+
if [[ "$CURRENT_MONTH" == "10" ]]; then
107+
gh pr edit "$PR_NUMBER" --add-label "hacktoberfest-accepted"
108+
fi
109+
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: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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+
if (!file) continue;
153+
let fileContent = await fs.readFile(file, 'utf-8')
154+
155+
if(!fileContent){
156+
feedbackPush(`Missing content, add the code for the file \`${file}\``)
157+
} else {
158+
HTMLReviews = reviewHTMLFile(file, fileContent) || HTMLReviews
159+
CSSReviews = reviewCSSFile(file, fileContent) || CSSReviews
160+
JSONReviews = reviewJSONFile(file, fileContent) || JSONReviews
161+
}
162+
}
163+
feedbacks = [
164+
...feedbacks,
165+
...HTMLReviews,
166+
...CSSReviews,
167+
...JSONReviews,
168+
]
169+
const otherReviews = reviewContribution(contributionStates)
170+
return ([
171+
...feedbacks,
172+
...otherReviews
173+
])
174+
}
175+
176+
const generateReviewMessage = (feedbacks ) => {
177+
const messageLines = [`Aloha @${contributorHandle} 🙌 Thanks for your contribution!`]
178+
const messagePush = createFeedbacks(messageLines, 'line')
179+
180+
181+
if( feedbacks.length ) {
182+
messagePush("Before we could merge, please address the following:")
183+
messagePush("## Feedbacks")
184+
185+
const taskList = feedbacks.join("\n")
186+
messagePush(`${ taskList }`)
187+
} else {
188+
messagePush("Seems to meet requirements, now awaiting for a maintener last validation.")
189+
}
190+
191+
192+
messageLines.push( "Happy Coding! 🚀")
193+
const messageReview = messageLines.join("\n")
194+
return messageReview
195+
}
196+
197+
;(async () => {
198+
const contributionState = getContributionState()
199+
const feedbacks = await checkContent(contributionState)
200+
const PRFinalReview = generateReviewMessage(feedbacks)
201+
console.info(PRFinalReview)
202+
})()

0 commit comments

Comments
 (0)