@@ -22,14 +22,14 @@ jobs:
2222 - name : Checkout repository
2323 uses : actions/checkout@v4
2424
25- - name : Parse issue and create/update PR
25+ - name : Parse issue fields
26+ id : parse
2627 uses : actions/github-script@v7
2728 with :
2829 script : |
2930 const issue = context.payload.issue;
3031 const body = issue.body;
31- const action = context.payload.action;
32-
32+
3333 // Parse the issue form fields
3434 function parseField(body, fieldName) {
3535 const regex = new RegExp(`### ${fieldName}\\s*\\n\\s*([\\s\\S]*?)(?=\\n### |$)`, 'i');
@@ -39,12 +39,12 @@ jobs:
3939 }
4040 return '';
4141 }
42-
42+
4343 function parseCheckboxes(body, fieldName) {
4444 const regex = new RegExp(`### ${fieldName}\\s*\\n([\\s\\S]*?)(?=\\n### |$)`, 'i');
4545 const match = body.match(regex);
4646 if (!match) return [];
47-
47+
4848 const checkboxSection = match[1];
4949 const checked = [];
5050 const checkboxRegex = /- \[x\] (.+)/gi;
@@ -54,13 +54,13 @@ jobs:
5454 }
5555 return checked;
5656 }
57-
57+
5858 const title = parseField(body, 'Resource Title');
5959 const url = parseField(body, 'URL');
6060 const description = parseField(body, 'Description');
6161 const dialect = parseField(body, 'Dialect / Section');
6262 const tags = parseCheckboxes(body, 'Tags');
63-
63+
6464 if (!title || !url || !description || !dialect) {
6565 console.log('Missing required fields, skipping PR creation');
6666 await github.rest.issues.createComment({
@@ -69,99 +69,151 @@ jobs:
6969 issue_number: issue.number,
7070 body: '⚠️ Could not parse all required fields from this submission. Please ensure Title, URL, Description, and Dialect are filled out.'
7171 });
72+ core.setOutput('skip', 'true');
7273 return;
7374 }
74-
75- // Map dialect to section function name and insertion marker
75+
76+ // Map dialect to section ID
7677 const dialectMap = {
77- 'General Lisp Resources': { section: 'general-section', id: 'general' } ,
78- 'Common Lisp': { section: 'common-lisp-section', id: 'common-lisp' } ,
79- 'Scheme': { section: 'scheme-section', id: 'scheme' } ,
80- 'Racket': { section: 'racket-section', id: 'racket' } ,
81- 'Clojure': { section: 'clojure-section', id: 'clojure' } ,
82- 'Emacs Lisp': { section: 'emacs-lisp-section', id: 'emacs-lisp' } ,
83- 'Janet': { section: 'janet-section', id: 'janet' } ,
84- 'Other Lisps & Inspired Languages': { section: 'others-section', id: 'others' }
78+ 'General Lisp Resources': 'general' ,
79+ 'Common Lisp': 'common-lisp' ,
80+ 'Scheme': 'scheme' ,
81+ 'Racket': 'racket' ,
82+ 'Clojure': 'clojure' ,
83+ 'Emacs Lisp': 'emacs-lisp' ,
84+ 'Janet': 'janet' ,
85+ 'Other Lisps & Inspired Languages': 'others'
8586 };
86-
87- const sectionInfo = dialectMap[dialect];
88- if (!sectionInfo ) {
87+
88+ const sectionId = dialectMap[dialect];
89+ if (!sectionId ) {
8990 console.log('Unknown dialect:', dialect);
91+ await github.rest.issues.createComment({
92+ owner: context.repo.owner,
93+ repo: context.repo.repo,
94+ issue_number: issue.number,
95+ body: `⚠️ Unknown dialect: "${dialect}". Please select a valid option.`
96+ });
97+ core.setOutput('skip', 'true');
9098 return;
9199 }
92-
93- // Format tags for Scheme
94- const tagsScheme = tags.length > 0
95- ? `'(${tags.map(t => `"${t}"`).join(' ')})`
96- : "'()";
97-
98- // Escape special characters for Scheme strings
99- const escapeScheme = (str) => str.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
100-
101- // Create the resource-card s-expression
102- const resourceCard = [
103- ' ,(resource-card',
104- ` "${escapeScheme(title)}"`,
105- ` "${escapeScheme(url)}"`,
106- ` "${escapeScheme(description)}"`,
107- ` ${tagsScheme})`
108- ].join('\n');
109-
110- // Consistent branch name based on issue number only
100+
101+ // Output parsed values
102+ core.setOutput('skip', 'false');
103+ core.setOutput('title', title);
104+ core.setOutput('url', url);
105+ core.setOutput('description', description);
106+ core.setOutput('dialect', dialect);
107+ core.setOutput('section_id', sectionId);
108+ core.setOutput('tags', JSON.stringify(tags));
109+ core.setOutput('tags_json', JSON.stringify(tags));
110+
111+ - name : Modify index.scm with Scheme script
112+ if : steps.parse.outputs.skip != 'true'
113+ id : modify
114+ run : |
115+ # Escape single quotes for shell by replacing ' with '\''
116+ escape_shell() {
117+ echo "$1" | sed "s/'/'\\\\''/g"
118+ }
119+
120+ TITLE='${{ steps.parse.outputs.title }}'
121+ URL='${{ steps.parse.outputs.url }}'
122+ DESC='${{ steps.parse.outputs.description }}'
123+ SECTION_ID='${{ steps.parse.outputs.section_id }}'
124+ TAGS_JSON='${{ steps.parse.outputs.tags_json }}'
125+
126+ echo "Running Scheme script to add resource..."
127+ echo "Section ID: $SECTION_ID"
128+ echo "Title: $TITLE"
129+ echo "URL: $URL"
130+
131+ # Run the docker compose command
132+ if docker compose run --rm build scripts/add-resource.scm --in-place "$SECTION_ID" "$TITLE" "$URL" "$DESC" "$TAGS_JSON"; then
133+ echo "success=true" >> $GITHUB_OUTPUT
134+ echo "Resource added successfully"
135+ else
136+ EXIT_CODE=$?
137+ echo "success=false" >> $GITHUB_OUTPUT
138+ echo "exit_code=$EXIT_CODE" >> $GITHUB_OUTPUT
139+
140+ # Map exit code to error message
141+ case $EXIT_CODE in
142+ 1)
143+ ERROR_MSG="Invalid arguments provided to the script."
144+ ;;
145+ 2)
146+ ERROR_MSG="Could not find pages/index.scm file."
147+ ;;
148+ 3)
149+ ERROR_MSG="Parse/syntax error in the Scheme file."
150+ ;;
151+ 4)
152+ ERROR_MSG="Could not find the section in index.scm."
153+ ;;
154+ 5)
155+ ERROR_MSG="Validation failed. The generated content is invalid."
156+ ;;
157+ *)
158+ ERROR_MSG="Unknown error occurred (exit code: $EXIT_CODE)."
159+ ;;
160+ esac
161+
162+ echo "error_message=$ERROR_MSG" >> $GITHUB_OUTPUT
163+ echo "Script failed with exit code $EXIT_CODE: $ERROR_MSG"
164+ exit 1
165+ fi
166+
167+ - name : Handle script failure
168+ if : failure() && steps.modify.outputs.success == 'false'
169+ uses : actions/github-script@v7
170+ with :
171+ script : |
172+ const errorMessage = '${{ steps.modify.outputs.error_message }}';
173+ const dialect = '${{ steps.parse.outputs.dialect }}';
174+
175+ await github.rest.issues.createComment({
176+ owner: context.repo.owner,
177+ repo: context.repo.repo,
178+ issue_number: context.payload.issue.number,
179+ body: `⚠️ Failed to add resource: ${errorMessage}\n\nPlease check the workflow logs for more details, or contact a maintainer if the issue persists.`
180+ });
181+
182+ - name : Create or update PR
183+ if : steps.parse.outputs.skip != 'true' && steps.modify.outputs.success == 'true'
184+ uses : actions/github-script@v7
185+ with :
186+ script : |
187+ const issue = context.payload.issue;
188+ const title = '${{ steps.parse.outputs.title }}';
189+ const url = '${{ steps.parse.outputs.url }}';
190+ const description = '${{ steps.parse.outputs.description }}';
191+ const dialect = '${{ steps.parse.outputs.dialect }}';
192+ const tags = JSON.parse('${{ steps.parse.outputs.tags }}');
193+
111194 const branchName = `resource/issue-${issue.number}`;
112-
195+
113196 // Check if a PR already exists for this issue
114197 const { data: pulls } = await github.rest.pulls.list({
115198 owner: context.repo.owner,
116199 repo: context.repo.repo,
117200 head: `${context.repo.owner}:${branchName}`,
118201 state: 'open'
119202 });
120-
203+
121204 const existingPR = pulls.length > 0 ? pulls[0] : null;
122-
123- // Get the default branch SHA (latest main )
205+
206+ // Get the current commit SHA (the modified index.scm is in the working directory )
124207 const { data: mainRef } = await github.rest.git.getRef({
125208 owner: context.repo.owner,
126209 repo: context.repo.repo,
127210 ref: 'heads/main'
128211 });
129-
130- // Get the latest index.scm content from main
131- const { data: mainFileData } = await github.rest.repos.getContent({
132- owner: context.repo.owner,
133- repo: context.repo.repo,
134- path: 'pages/index.scm',
135- ref: 'main'
136- });
137-
138- const content = Buffer.from(mainFileData.content, 'base64').toString('utf8');
139-
140- // Find the section and insert before the closing ))) of the div/section/quasiquote
141- // Match up to and including all complete resource-cards, then capture just the 3 structural closing parens
142- const sectionRegex = new RegExp(
143- `(\\(section \\(@ \\(id "${sectionInfo.id}"\\)[\\s\\S]*?\\(div \\(@ \\(class "resources-grid"\\)\\)[\\s\\S]*?)(\\)\\)\\))`,
144- 'm'
145- );
146-
147- const sectionMatch = content.match(sectionRegex);
148- if (!sectionMatch) {
149- console.log('Could not find section:', sectionInfo.id);
150- await github.rest.issues.createComment({
151- owner: context.repo.owner,
152- repo: context.repo.repo,
153- issue_number: issue.number,
154- body: `⚠️ Could not find the "${dialect}" section in index.scm. Manual intervention required.`
155- });
156- return;
157- }
158-
159- // Insert the new resource card before the closing )))))
160- const newContent = content.replace(
161- sectionRegex,
162- `$1\n${resourceCard}$2`
163- );
164-
212+
213+ // Read the modified index.scm file
214+ const fs = require('fs');
215+ const modifiedContent = fs.readFileSync('pages/index.scm', 'utf8');
216+
165217 const prBody = [
166218 '## New Resource Submission',
167219 '',
@@ -180,11 +232,11 @@ jobs:
180232 '',
181233 `Closes #${issue.number}`
182234 ].join('\n');
183-
235+
184236 if (existingPR) {
185237 // UPDATE existing PR: reset branch to main and apply new changes
186238 console.log(`Updating existing PR #${existingPR.number}`);
187-
239+
188240 // Force update the branch ref to point to main
189241 await github.rest.git.updateRef({
190242 owner: context.repo.owner,
@@ -193,26 +245,26 @@ jobs:
193245 sha: mainRef.object.sha,
194246 force: true
195247 });
196-
248+
197249 // Get the file SHA from the reset branch (now same as main)
198250 const { data: branchFileData } = await github.rest.repos.getContent({
199251 owner: context.repo.owner,
200252 repo: context.repo.repo,
201253 path: 'pages/index.scm',
202254 ref: branchName
203255 });
204-
256+
205257 // Update the file with new content
206258 await github.rest.repos.createOrUpdateFileContents({
207259 owner: context.repo.owner,
208260 repo: context.repo.repo,
209261 path: 'pages/index.scm',
210262 message: `Update resource: ${title}`,
211- content: Buffer.from(newContent ).toString('base64'),
263+ content: Buffer.from(modifiedContent ).toString('base64'),
212264 sha: branchFileData.sha,
213265 branch: branchName
214266 });
215-
267+
216268 // Update PR title and body
217269 await github.rest.pulls.update({
218270 owner: context.repo.owner,
@@ -221,21 +273,21 @@ jobs:
221273 title: `Add resource: ${title}`,
222274 body: prBody
223275 });
224-
276+
225277 // Comment on the issue about the update
226278 await github.rest.issues.createComment({
227279 owner: context.repo.owner,
228280 repo: context.repo.repo,
229281 issue_number: issue.number,
230282 body: `🔄 Pull request #${existingPR.number} has been updated with your changes.`
231283 });
232-
284+
233285 console.log(`Updated PR #${existingPR.number} for issue #${issue.number}`);
234-
286+
235287 } else {
236288 // CREATE new PR
237289 console.log('Creating new PR');
238-
290+
239291 // Create new branch from main
240292 try {
241293 await github.rest.git.createRef({
@@ -258,26 +310,26 @@ jobs:
258310 throw e;
259311 }
260312 }
261-
313+
262314 // Get the file SHA from the new branch
263315 const { data: branchFileData } = await github.rest.repos.getContent({
264316 owner: context.repo.owner,
265317 repo: context.repo.repo,
266318 path: 'pages/index.scm',
267319 ref: branchName
268320 });
269-
321+
270322 // Update the file
271323 await github.rest.repos.createOrUpdateFileContents({
272324 owner: context.repo.owner,
273325 repo: context.repo.repo,
274326 path: 'pages/index.scm',
275327 message: `Add resource: ${title}`,
276- content: Buffer.from(newContent ).toString('base64'),
328+ content: Buffer.from(modifiedContent ).toString('base64'),
277329 sha: branchFileData.sha,
278330 branch: branchName
279331 });
280-
332+
281333 // Create the pull request
282334 const { data: pr } = await github.rest.pulls.create({
283335 owner: context.repo.owner,
@@ -287,14 +339,14 @@ jobs:
287339 base: 'main',
288340 body: prBody
289341 });
290-
342+
291343 // Comment on the issue with the PR link
292344 await github.rest.issues.createComment({
293345 owner: context.repo.owner,
294346 repo: context.repo.repo,
295347 issue_number: issue.number,
296348 body: `🎉 Thanks for your submission! A pull request has been automatically created: #${pr.number}\n\nA maintainer will review it shortly.`
297349 });
298-
350+
299351 console.log(`Created PR #${pr.number} for issue #${issue.number}`);
300352 }
0 commit comments