Just so you know, this ain't no suspenseful action story, but a tale of fun action as a Software Freestyle Engineer.
As an SFE, I'm hella lucky to get to know so many peeps who dedicate themselves not just to money, ya feel me? Does that sound too provocative?
Money's important and all, but it can't get you everything, y'all! - Me
Anyway, back to the topic at hand, homie! This action is a crucial component of the Metaphore Story - SCP, 'cause with this action, all the stories are distributed to the public.
The Idea of The Action
It's amazing how @mkubdev simple yet brilliant idea sparked my passion as a Freestyler to craft the words into a phenomenal story wrapped in a metaphorical style.
The Action Struct
The Template Story
The story template serves as a placeholder for a story that will be distributed based on the closed issue.
---
layout: post
title: {title}
author: {author}
created_at: {created_at}
language: {language}
---
{content}
The Slugify and Git File
Slugify
This here is some JavaScript module called "slugify.js", my friend! The goal is to take some text string as input and turn it into a "slug" format, ya dig? A "slug" is a URL-friendly version of a string, where all the characters are in lowercase, spaces are replaced with hyphens ("-"), and all non-letter characters are removed. And I just had to write it all in one line, no surprises there, man!
// Filename: slugify.js
module.exports = text => text.toString().normalize('NFKD').toLowerCase().trim().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-').replace(/\-$/g, '');
Check it out, punk! Here's the lowdown in just one funky fresh line!
This code exports a function as a module so it can be imported into other files. It takes an input text and casts it to a string type (just in case it isn't already a string). It normalizes the string using the Unicode normalization form "NFKD", which replaces accented characters with their base equivalents. It converts the string to lowercase. It removes leading or trailing white space from the string (optional). It replaces one or more consecutive white space characters with a single hyphen ("-"). It removes all non-word characters (including spaces) from the string. It replaces any consecutive occurrence of two or more hyphens with a single hyphen. It removes any remaining hyphens from the string.
So when you use this function on a text string, it will return a new string that has been transformed into a slug format, making it easier to use as a URL or identifier.
Git
// Filename: git.js
module.exports = {
ghBotUsername: 'github-actions[bot]',
ghBotEmail: '41898282+github-actions[bot]@users.noreply.github.com',
}
Yo yo yo! Check out this code, dawg! It's all about this bot, right? The bot has a name, man! It's called ghBotUsername
! And that's not just any name, bro! It's the name of github-actions[bot]
! That's like, super cool, right?
And wait, wait! The bot even has an email, man! It's ghBotEmail
! And it's like, really crazy! It has all these numbers and symbols and stuff! It's like 41898282+github-actions[bot]@users.noreply.github.com
! Can you even handle it? It seems like this bot is totally legit, with its own special email and everything!
The Action YAY-ML 🤣
name: "Check Closed Issues and Generate New Story"
description: "Punk will check cloesed Issue and generate story"
inputs:
github-token:
description: "GitHub token for repo"
required: true
issue-message:
description: "Message to reply to new issue as a comment"
default: "Thank you for creating an Issue and contributing to our community project :tada:. Someone from the community will get back to you soon, usually within 24 hours"
pr-message:
description: "Message to reply to new pull request as a comment"
default: "Thank you for creating a Pull Request and contributing to our community project :tada:. Someone from the community will get back to you soon, usually within 24 hours"
footer:
description: "Append issue and pull request message with this message"
default: ""
runs:
using: "node16"
main: "index.js"
Wait a minute, why are there so many input parameters there? OhMyPunk!!!
Ok, I'll explain it very briefly. So here's the explanation 👆👀👆.
Yoooooo, you can shake to find out why there are some input parameters in the GitHub Action file above.
(Note: "Shake" here means "scroll up and down quickly to see the context" in a casual and playful way.) 😕
The Main Action
I intentionally didn't write the steps sequentially because they are already very common and ordinary. Consider this post as a truly abstract, abstract painting.
// Filename: index.js
const core = require('@actions/core');
const github = require('@actions/github');
const greetingContributor = require('./scripts/greetingContributor');
const storyGenerator = require('./scripts/storyGenerator');
(async () => {
try {
const githubToken = core.getInput('github-token', { required: true });
const issueMessage = core.getInput('issue-message');
const prMessage = core.getInput('pr-message');
const footer = core.getInput('footer');
const client = github.getOctokit(githubToken);
const context = github.context;
switch (context.payload.action) {
case 'closed':
await storyGenerator(client, context)
break;
case 'opened':
await greetingContributor(client, context, issueMessage, prMessage, footer)
break;
default:
console.log('No action, skipping');
core.notice('No action, skipping!');
break;
}
} catch (error) {
core.setFailed(error.message);
}
})()
Inject The Core Pack (Whatever you called)
You certainly saw these lines of code above, didn't you?
const core = require('@actions/core');
const github = require('@actions/github');
Yup! Inject those 2 packages with 😝 composer
npm
npm install @actions/core @action/github
And you also saw this line of code:
const greetingContributor = require('./scripts/greetingContributor');
const storyGenerator = require('./scripts/storyGenerator');
So, where do they come from and what do they look like?
The Greeting Contributor
An action to greet the contributor in the StreetCommunityProgrammer/metaphore
GitHub repository.
const core = require('@actions/core');
module.exports = async (client, context, issueMessage, prMessage, footer) => {
try {
const issue = await client.rest.issues.get({
owner: context.issue.owner,
repo: context.issue.repo,
issue_number: context.issue.number,
})
const issueData = issue.data
const labels = issueData.labels.map(label => label.name)
if (context.payload.action !== 'opened' || labels.includes('story::comment') === true) {
console.log('No issue / pull request was opened, skipping');
return;
}
const footerTags = `<p>${footer}</p>`;
if (!!context.payload.issue) {
await client.rest.issues.createComment({
owner: context.issue.owner,
repo: context.issue.repo,
issue_number: context.issue.number,
body: issueMessage + footerTags
});
} else {
await client.rest.pulls.createReview({
owner: context.issue.owner,
repo: context.issue.repo,
pull_number: context.issue.number,
body: prMessage + footerTags,
event: 'COMMENT'
});
}
} catch (error) {
core.setFailed(error.message);
}
}
When the action greets the contributor is triggered (when context.payload.action
=== opened
), the result will be like this.
The Story Action
Hmmmmmmmmm.... this is the most important feature and functionality of the Metaphore Story - SCP website. Because all the stories displayed on the website are created from here.
module.exports = async (client, context) => {
try {
const issue = await client.rest.issues.get({
owner: context.issue.owner,
repo: context.issue.repo,
issue_number: context.issue.number,
})
const assignees = issue.data.assignees
const isReviewerPresence = assignees.some(assignee => ['darkterminal', 'mkubdev'].includes(assignee.login))
if (issue.data.state === 'closed' && isReviewerPresence) {
const labels = issue.data.labels.map(label => label.name)
const metaphors = [
['css', 'css'],
['golang', 'golang'],
['javascript', 'javascript'],
['java', 'java'],
['maths', 'maths'],
['python', 'python'],
['php', 'php'],
['physics', 'physics'],
['ruby', 'ruby'],
['rust', 'rust'],
['zig', 'zig']
]
const isMetaphor = metaphors.some(([category, label]) => labels.every(l => ['metaphore', category].includes(l)))
if (isMetaphor) {
const [category, label] = metaphors.find(([category, label]) => labels.every(l => ['metaphore', category].includes(l)))
console.log(`Is ${category} metaphor`)
createMetaphorFile(client, issue.data, context, label)
}
addLabelToClosedIssue(client, context.issue.owner, context.issue.repo, context.issue.number, [...labels, 'published'])
}
} catch (error) {
console.log(`Error on storyGenerator: ${error}`)
return false
}
}
And this is the code I had before writing this post, it looks really embarrassing and definitely goes against the principles of "A Clean Code" 😎.
module.exports = async (client, context) => {
try {
const issue = await client.rest.issues.get({
owner: context.issue.owner,
repo: context.issue.repo,
issue_number: context.issue.number,
})
const issueData = issue.data
const assignees = issue.data.assignees
const isReviewerPresence = assignees.some(assignee => {
return assignee.login === "darkterminal" || assignee.login === "mkubdev";
});
if (issueData.state === 'closed' && isReviewerPresence) {
const labels = issueData.labels.map(label => label.name)
// Metaphor Categories
const isCssMetaphor = labels.every(label => ['metaphore', 'css'].includes(label))
const isGolangMetaphor = labels.every(label => ['metaphore', 'golang'].includes(label))
const isJavaScriptMetaphor = labels.every(label => ['metaphore', 'javascript'].includes(label))
const isJavaMetaphor = labels.every(label => ['metaphore', 'java'].includes(label))
const isMathsMetaphor = labels.every(label => ['metaphore', 'maths'].includes(label))
const isPythonMetaphor = labels.every(label => ['metaphore', 'python'].includes(label))
const isPhpMetaphor = labels.every(label => ['metaphore', 'php'].includes(label))
const isPhysicsMetaphor = labels.every(label => ['metaphore', 'physics'].includes(label))
const isRubyMetaphor = labels.every(label => ['metaphore', 'ruby'].includes(label))
const isRustMetaphor = labels.every(label => ['metaphore', 'rust'].includes(label))
const isZigMetaphor = labels.every(label => ['metaphore', 'zig'].includes(label))
if (isCssMetaphor) {
console.log(`Is css metaphor`)
createMetaphorFile(client, issueData, context, 'css')
} else if (isGolangMetaphor) {
console.log(`Is golang metaphor`)
createMetaphorFile(client, issueData, context, 'golang')
} else if (isJavaScriptMetaphor) {
console.log(`Is javascript metaphor`)
createMetaphorFile(client, issueData, context, 'javascript')
} else if (isJavaMetaphor) {
console.log(`Is java metaphor`)
createMetaphorFile(client, issueData, context, 'java')
} else if (isMathsMetaphor) {
console.log(`Is maths metaphor`)
createMetaphorFile(client, issueData, context, 'maths')
} else if (isPythonMetaphor) {
console.log(`Is python metaphor`)
createMetaphorFile(client, issueData, context, 'python')
} else if (isPhpMetaphor) {
console.log(`Is php metaphor`)
createMetaphorFile(client, issueData, context, 'php')
} else if (isPhysicsMetaphor) {
console.log(`Is physics metaphor`)
createMetaphorFile(client, issueData, context, 'physics')
} else if (isRubyMetaphor) {
console.log(`Is ruby metaphor`)
createMetaphorFile(client, issueData, context, 'ruby')
} else if (isRustMetaphor) {
console.log(`Is rust metaphor`)
createMetaphorFile(client, issueData, context, 'rust')
} else if (isZigMetaphor) {
console.log(`Is zig metaphor`)
createMetaphorFile(client, issueData, context, 'zig')
}
addLabelToClosedIssue(client, context.issue.owner, context.issue.repo, context.issue.number, [...labels, 'published'])
}
} catch (error) {
console.log(`Erorr on storyGenerator: ${error}`)
return false
}
}
Man, there's just way too many if-else
statements in this code. We call this Barbarian Coding
where I'm from, ha ha! 😝
Another Func That Not Yet Mentioned
createMetaphorFile
, addLabelToClosedIssue
, dan createFileContent
. 3 fungsi yang sangat indah untuk diceritakan.
1. The createMetaphorFile
Function
/**
* Creates a new metaphor file in the specified category, based on the provided issue data and context.
* @async
* @function createMetaphorFile
* @param {Object} client - The client object containing information about the GitHub repository and issue, including owner, repo, and issue number.
* @param {Object} issueData - The issue data object containing information about the issue, including title, user, created date, and body content.
* @param {Object} context - The context object containing information about the GitHub repository and issue, including owner, repo, and issue number.
* @param {string} category - The category of the metaphor file to create.
* @returns {Promise} A Promise that resolves when the metaphor file has been created in the GitHub repository.
*/
async function createMetaphorFile(client, issueData, context, category) {
const metaphorTitle = slugify(issueData.title);
const template = `---
layout: post
title: {title}
author: {author}
created_at: {created_at}
language: {language}
---
{content}`;
const placeholders = ['{title}', '{author}', '{created_at}', '{language}', '{content}'];
const values = [
issueData.title,
issueData.user.login,
issueData.created_at,
category,
issueData.body,
];
const replacedTemplate = placeholders.reduce((template, placeholder, index) => {
return template.replace(new RegExp(placeholder, 'g'), values[index]);
}, template);
console.log('Replacement result: ' + JSON.stringify(replacedTemplate, undefined, 2))
const metaphorContent = Buffer.from(replacedTemplate).toString('base64');
const createContent = await createFileContent({
client: client,
owner: context.issue.owner,
repo: context.issue.repo,
path: `public/collections/stories/${category}/${metaphorTitle}.md`,
message: `docs(generate): new metaphor from @${issueData.user.login}`,
content: metaphorContent,
});
console.log(`Content Metadata: ${JSON.stringify(createContent, undefined, 2)}`);
}
let me tell you a funky story about the magical function "createMetaphorFile". This function was like a superhero, with the power to create a brand new metaphor file using data from a GitHub issue. And it did all this using the funky language called JavaScript.
This superhero function had some special helpers to make its job easier. They were named client, issueData, and context, and they were like the sidekicks to createMetaphorFile. They helped it read and write to GitHub repositories and get important info about the issue that the metaphor would be based on.
But the most important part of createMetaphorFile was its secret code called "category". This was like a magical spell that would transform the raw issue data into a groovy new story.
The first thing createMetaphorFile did was to use a spell called "slugify" to turn the title of the issue into a slug. It was like turning a regular sentence into a funky, lowercase string with no spaces or special characters.
Next, createMetaphorFile whipped up a special template for the new metaphor. This template had some placeholders for the title, author, created date, language, and content of the metaphor. It was like a blueprint for the new story.
Then, createMetaphorFile busted out another spell called "reduce". This helped it fill in the blanks of the template by replacing each placeholder with the corresponding value from the issue data. It was like painting a new picture using the colors from an old one.
After that, createMetaphorFile used one more spell to transform the completed template into a funky, computer-friendly code called "base64". This code was like a secret language that only computers could understand.
And finally, createMetaphorFile used its GitHub magic to create a brand new file in the repository. It gave the file a cool name based on the slugified title, and it put the completed template in base64 format inside the file as the content.
And just like that, createMetaphorFile had created a dope new metaphor using the power of GitHub and JavaScript.
2. The createFileContent
Function
/**
* Creates or updates a file in a GitHub repository with the specified content.
* @async
* @function createFileContent
* @param {Object} options - An object containing options for the file creation/update operation.
* @param {string} options.owner - The owner of the GitHub repository.
* @param {string} options.repo - The name of the GitHub repository.
* @param {string} options.path - The path to the file in the repository.
* @param {string} options.message - The commit message to use for the file update/creation.
* @param {string} options.content - The content of the file to create/update, encoded in base64.
* @returns {Promise<Object>} A Promise that resolves with the metadata for the created/updated file.
*/
async function createFileContent({ client, owner, repo, path, message, content }) {
return await client.rest.repos.createOrUpdateFileContents({
owner,
repo,
path,
message,
content,
committer: {
name: ghBotUsername,
email: ghBotEmail
},
author: {
name: ghBotUsername,
email: ghBotEmail
}
});
}
So, there's this function called "createFileContent" that's totally lit! It's written in JavaScript, which is like the coolest language out there.
Basically, createFileContent is like a messenger that helps you talk to GitHub. It can create or update a file in a GitHub repository, which is like a big shared folder where lots of people can work together on projects.
To use createFileContent, you have to give it some options. These options are like instructions that tell createFileContent what to do. You need to tell it who the owner of the repository is, what the name of the repository is, where in the repository you want to put the file, what the message for the commit should be, and most importantly, what the content of the file should be.
Once you've given createFileContent all the options it needs, it goes to work. It uses some magic GitHub spells to create or update the file in the repository with the content you specified. And just like that, your file is saved and ready to be shared with the world!
When createFileContent is done, it gives you back some metadata about the file it created or updated. This metadata is like information about the file, such as when it was last updated, who updated it, and what the file's name and path are.
So there you have it, createFileContent is a totally chill function that helps you create and update files in a GitHub repository like a 🐶!
3. The addLabelToClosedIssue
Function
/**
* Adds labels to a closed issue.
*
* @param {Object} client - The authenticated Octokit REST client.
* @param {string} owner - The owner of the repository.
* @param {string} repo - The name of the repository.
* @param {number} issue_number - The number of the issue to add labels to.
* @param {Array} [labels] - An array of labels to add to the issue.
* @returns {Promise<void>} A Promise that resolves when the labels have been added to the issue.
*/
async function addLabelToClosedIssue(client, owner, repo, issue_number, labels) {
await client.rest.issues.addLabels({
owner,
repo,
issue_number,
labels
})
console.log(`Label added: ${labels.join(', ')}`)
}
This code is about adding labels to a closed issue on a website called GitHub.
When someone creates an issue, they can add labels to it to categorize it. Sometimes issues get closed, but you might still want to add labels to them to help people find them later. That's what this code does!
The code takes in four pieces of information: the owner of the repository (kind of like the boss of the project), the name of the repository (where the project lives), the number of the issue (like a code for the issue), and an optional array of labels to add to the issue.
Then, the code uses a magic spell called "addLabels" to add the labels to the issue. Once it's done, it says "Label added" and the names of the labels that were added.
Overall, this code is like adding stickers to a book you've already finished reading, so that when you want to find it again later, you can just look for the stickers!
Thanks to @mkubdev
The Full Metaphor Story
StreetCommunityProgrammer / action-collections
This is repository of SCP Action Collections
SCP Action Collections
This is repository of SCP Action Collections
Top comments (2)
Thanks a lot Ali! It was a very fun adventure! Long live Metaphore!
Spread our punk vibes to the world!!! 🛸