From ee0c65d9c8d3907351fc9864f0d929443e2416d8 Mon Sep 17 00:00:00 2001 From: alexiscolin Date: Sat, 5 Oct 2024 03:13:09 +0900 Subject: [PATCH 1/3] feat: add serverless auto-issue function --- .env.example | 4 ++ .github/workflows/create-issue.yaml | 28 +++++++++ .gitignore | 3 +- functions/createissue/github-access.js | 31 ++++++++++ functions/createissue/index.js | 54 ++++++++++++++++++ functions/createissue/issue-templates.js | 73 ++++++++++++++++++++++++ netlify.toml | 5 ++ package.json | 5 +- 8 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/create-issue.yaml create mode 100644 functions/createissue/github-access.js create mode 100644 functions/createissue/index.js create mode 100644 functions/createissue/issue-templates.js create mode 100644 netlify.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f3b28ff --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +GITHUB_APP_ID=your-github-app-id +GITHUB_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...your GH app private key here - 1 line...\n-----END PRIVATE KEY-----" +GITHUB_INSTALLATION_ID=your-github-app-installation-id +GITHUB_REPO_ISSUES_ENDPOINT=https://api.github.com/repos/allinbits/wallet-security-lists/issues diff --git a/.github/workflows/create-issue.yaml b/.github/workflows/create-issue.yaml new file mode 100644 index 0000000..5adff0d --- /dev/null +++ b/.github/workflows/create-issue.yaml @@ -0,0 +1,28 @@ +name: Create GitHub Issue + +on: + workflow_dispatch: + inputs: + title: + description: "Issue's title" + required: true + type: string + body: + description: "Issue's body" + required: true + type: string + +jobs: + create-issue: + runs-on: ubuntu-latest + permissions: + contents: write + issues: write + + steps: + - name: Create a GitHub issue + uses: peter-evans/create-issue@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: ${{ github.event.inputs.title }} + body: ${{ github.event.inputs.body }} diff --git a/.gitignore b/.gitignore index b512c09..1dcef2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +.env \ No newline at end of file diff --git a/functions/createissue/github-access.js b/functions/createissue/github-access.js new file mode 100644 index 0000000..ed0a70c --- /dev/null +++ b/functions/createissue/github-access.js @@ -0,0 +1,31 @@ +const fetch = require("node-fetch"); + +const privateKey = process.env.GITHUB_PRIVATE_KEY.replace(/\\n/g, "\n"); + +const generateJWT = () => { + const payload = { + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(Date.now() / 1000) + 600, + iss: process.env.GITHUB_APP_ID, + }; + return require("jsonwebtoken").sign(payload, privateKey, { algorithm: "RS256" }); +}; + +const getInstallationToken = async (installationId) => { + const jwtToken = generateJWT(); + + const response = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, { + method: "POST", + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: "application/vnd.github.v3+json", + }, + }); + + const data = await response.json(); + return data.token; +}; + +module.exports = { + getInstallationToken, +}; diff --git a/functions/createissue/index.js b/functions/createissue/index.js new file mode 100644 index 0000000..436d12c --- /dev/null +++ b/functions/createissue/index.js @@ -0,0 +1,54 @@ +const { getInstallationToken } = require("./github-access"); +const { fetchYamlTemplate, convertYamlToMarkdown, getSupportedTypes } = require("./issue-templates"); + +exports.handler = async function (event, context) { + const { type, data } = JSON.parse(event.body); + + try { + const supportedTypes = getSupportedTypes(); + + if (!supportedTypes.includes(type)) { + return { + statusCode: 400, + body: JSON.stringify({ message: "Issue type not recognized" }), + }; + } + + const installationId = process.env.GITHUB_INSTALLATION_ID; + const token = await getInstallationToken(installationId); + + const yamlContent = fetchYamlTemplate(`${type.toLowerCase()}-add.yaml`); + const issueBody = convertYamlToMarkdown(yamlContent, data); + + const response = await fetch(process.env.GITHUB_REPO_ISSUES_ENDPOINT, { + method: "POST", + headers: { + Authorization: `token ${token}`, + Accept: "application/vnd.github.v3+json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: `[${type}-add]: ${data.name}`, + body: issueBody, + }), + }); + + if (response.ok) { + return { + statusCode: 200, + body: JSON.stringify({ message: "Issue created successfully" }), + }; + } else { + const error = await response.json(); + return { + statusCode: response.status, + body: JSON.stringify({ message: "Error creating issue", error }), + }; + } + } catch (error) { + return { + statusCode: 500, + body: JSON.stringify({ message: "Internal server error", error: error.message }), + }; + } +}; diff --git a/functions/createissue/issue-templates.js b/functions/createissue/issue-templates.js new file mode 100644 index 0000000..701d16c --- /dev/null +++ b/functions/createissue/issue-templates.js @@ -0,0 +1,73 @@ +const fs = require("fs"); +const path = require("path"); +const yaml = require("js-yaml"); + +const readTemplateFiles = () => { + try { + const templateDir = path.join(__dirname, "..", "..", ".github", "ISSUE_TEMPLATE"); + const files = fs.readdirSync(templateDir); + return files.map((file) => path.join(templateDir, file)); + } catch (error) { + throw new Error(`Error while reading template files: ${error.message}`); + } +}; + +const getSupportedTypes = () => { + try { + const files = readTemplateFiles(); + + const supportedTypes = files + .map((filePath) => { + const file = path.basename(filePath); + const match = file.match(/(\w+)-add\.yaml/); + return match ? match[1].charAt(0).toUpperCase() + match[1].slice(1) : null; + }) + .filter(Boolean); + + return supportedTypes; + } catch (error) { + throw new Error(`Error while retrieving supported types: ${error.message}`); + } +}; + +const fetchYamlTemplate = (templateFile) => { + try { + const files = readTemplateFiles(); + + const templatePath = files.find((filePath) => filePath.endsWith(templateFile)); + if (!templatePath) { + throw new Error(`Template ${templateFile} not found`); + } + + const fileContent = fs.readFileSync(templatePath, "utf8"); + return fileContent; + } catch (error) { + throw new Error(`Error while retrieving the local template: ${error.message}`); + } +}; + +const convertYamlToMarkdown = (yamlContent, formData) => { + const parsedYaml = yaml.load(yamlContent); + let markdownContent = ""; + + parsedYaml.body.forEach((field) => { + markdownContent += `### ${field.attributes.label}\n`; + + if (field.type === "input" || field.type === "textarea") { + markdownContent += `${formData[field.id] || field.attributes.placeholder}\n\n`; + } else if (field.type === "dropdown") { + const selectedValue = formData[field.id]; + const defaultValue = field.attributes.options[0]; + const finalValue = field.attributes.options.includes(selectedValue) ? selectedValue : defaultValue; + markdownContent += `${finalValue}\n\n`; + } + }); + + return markdownContent; +}; + +module.exports = { + getSupportedTypes, + fetchYamlTemplate, + convertYamlToMarkdown, +}; diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..56684c8 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,5 @@ +[build] + functions = "functions" + +[functions] + included_files = [".github/ISSUE_TEMPLATE/**"] \ No newline at end of file diff --git a/package.json b/package.json index 6dc5a00..6dcec58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wallet-security-lists", - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "index.js", "scripts": { @@ -10,6 +10,9 @@ "author": "", "license": "ISC", "dependencies": { + "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", + "node-fetch": "^2.7.0", "octokit": "^4.0.2" } } From 863905158ddd921f8d895f3907cdb7ec9559925b Mon Sep 17 00:00:00 2001 From: alexiscolin Date: Sat, 5 Oct 2024 13:57:21 +0900 Subject: [PATCH 2/3] chore: remove issue workflow --- .github/workflows/create-issue.yaml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .github/workflows/create-issue.yaml diff --git a/.github/workflows/create-issue.yaml b/.github/workflows/create-issue.yaml deleted file mode 100644 index 5adff0d..0000000 --- a/.github/workflows/create-issue.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Create GitHub Issue - -on: - workflow_dispatch: - inputs: - title: - description: "Issue's title" - required: true - type: string - body: - description: "Issue's body" - required: true - type: string - -jobs: - create-issue: - runs-on: ubuntu-latest - permissions: - contents: write - issues: write - - steps: - - name: Create a GitHub issue - uses: peter-evans/create-issue@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - title: ${{ github.event.inputs.title }} - body: ${{ github.event.inputs.body }} From 1c223e91c5694afe60326b6880103da099661c78 Mon Sep 17 00:00:00 2001 From: alexiscolin Date: Sat, 5 Oct 2024 14:00:00 +0900 Subject: [PATCH 3/3] chore: simplify func --- functions/createissue/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/createissue/index.js b/functions/createissue/index.js index 436d12c..39407ed 100644 --- a/functions/createissue/index.js +++ b/functions/createissue/index.js @@ -14,8 +14,7 @@ exports.handler = async function (event, context) { }; } - const installationId = process.env.GITHUB_INSTALLATION_ID; - const token = await getInstallationToken(installationId); + const token = await getInstallationToken(process.env.GITHUB_INSTALLATION_ID); const yamlContent = fetchYamlTemplate(`${type.toLowerCase()}-add.yaml`); const issueBody = convertYamlToMarkdown(yamlContent, data);