diff --git a/log-app/.github/workflows/log-app.yml b/log-app/.github/workflows/log-app.yml new file mode 100644 index 000000000..ae62a439a --- /dev/null +++ b/log-app/.github/workflows/log-app.yml @@ -0,0 +1,103 @@ +name : Deploy Log Web App to Cloudflare Worker + +on: + workflow_call: + inputs: + custom_domain: + required: false + type: string + production_branch: + required: false + type: string + build_artifact_name: + required: false + type: string + build_artifact_run_id: + required: false + type: string + secrets: + CLOUDFLARE_API_TOKEN: + required: true + CLOUDFLARE_ACCOUNT_ID: + required: false + UBIQUITY_BOUNTY_BOT_APP_ID: + required: true + UBIQUITY_BOUNTY_BOT_PRIVATE_KEY: + required: true + +jobs: + deploy-to-cloudflare: + name: Deploy to Cloudflare Pages + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: "20.3.0" + + - name: Build TypeScript + run: npm install && npm run build + + - name: Wrangler Install + run: npm install -g wrangler + + - name: "Download Artifact" + uses: actions/github-script@v6.3.3 + with: + script: | + const fs = require("fs"); + const downloadArtifact = require('${{ github.workspace }}/utils/download-artifact.js'); + const workflow_run_id = '${{ inputs.build_artifact_run_id || github.event.workflow_run.id }}'; + const artifact_name = '${{ inputs.build_artifact_name || 'pr' }}'; + const workspace = '${{ github.workspace }}'; + downloadArtifact({ github, context, fs, workflow_run_id, artifact_name, workspace }) + + - name: Extract Artifact + run: mkdir ./dist && unzip pr.zip && unzip pull-request.zip -d ./dist && ls + + - name: Publish to Cloudflare + id: publish-to-cloudflare + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + REPOSITORY: ${{ github.repository }} + PRODUCTION_BRANCH: ${{ inputs.production_branch || 'development' }} + OUTPUT_DIRECTORY: "./dist" + run: | + IFS='/' read -ra fields <<< "$REPOSITORY" + projectName="${fields[1]}" + projectName=$(echo $projectName | sed 's/\./-/g') + echo $projectName + wrangler pages project list > project_list.txt + if grep -q $projectName project_list.txt; then + echo "Project found" + else + echo "Project not found" + wrangler pages project create "$projectName" --production-branch "$PRODUCTION_BRANCH" + fi + wrangler pages deploy "$OUTPUT_DIRECTORY" --project-name "$projectName" > ./deployments.log + cat deployments.log + + - name: Get UbiquiBot Token + uses: tibdex/github-app-token@v1.7.0 + id: get_installation_token + with: + app_id: ${{ secrets.UBIQUITY_BOUNTY_BOT_APP_ID }} + private_key: ${{ secrets.UBIQUITY_BOUNTY_BOT_PRIVATE_KEY }} + + - name: Deploy Comment as UbiquiBot + uses: actions/github-script@v6 + with: + github-token: ${{ steps.get_installation_token.outputs.token }} + script: | + const fs = require("fs"); + const printDeploymentsLog = require('${{ github.workspace }}/utils/print-deployments-logs.js'); + const customDomain = "${{ inputs.custom_domain || '' }}"; + await printDeploymentsLog({ github, context, fs, customDomain }); diff --git a/log-app/utils/download-artifact.js b/log-app/utils/download-artifact.js new file mode 100644 index 000000000..2a426a667 --- /dev/null +++ b/log-app/utils/download-artifact.js @@ -0,0 +1,19 @@ +module.exports = async ({ github, context, fs, workflow_run_id, workspace, artifact_name }) => { + console.log("download_artifact....."); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: workflow_run_id, + }); + const matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == artifact_name; + })[0]; + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: "zip", + }); + fs.writeFileSync(`${workspace}/pr.zip`, Buffer.from(download.data)); + }; + \ No newline at end of file diff --git a/log-app/utils/print-deployments-logs.js b/log-app/utils/print-deployments-logs.js new file mode 100644 index 000000000..34e46356a --- /dev/null +++ b/log-app/utils/print-deployments-logs.js @@ -0,0 +1,167 @@ +//@ts-check +module.exports = async ({ github, context, fs, customDomain }) => { + const pullRequestInfo = fs.readFileSync("./pr_number").toString("utf-8"); + console.log({ pullRequestInfo }); + const infoSubstring = pullRequestInfo.split(","); + const eventName = infoSubstring[0].split("=")[1]; + const pullRequestNumber = infoSubstring[1].split("=")[1] ?? 0; + const commitSha = infoSubstring[2].split("=")[1]; + const deploymentsLog = fs.readFileSync("./deployments.log").toString("utf-8"); + + let defaultBody = deploymentsLog; + let uniqueDeployUrl = deploymentsLog.match(/https:\/\/.+\.pages\.dev/gim); + const botCommentsArray = []; + + if (uniqueDeployUrl) { + if (customDomain) { + uniqueDeployUrl = uniqueDeployUrl[0].replace(/\..+$/, `.${customDomain}`); + } + const slicedSha = commitSha.slice(0, -33); + + defaultBody = `- [${slicedSha}](${uniqueDeployUrl})`; + } + + const verifyInput = (data) => { + return data !== ""; + }; + + const GMTConverter = (bodyData) => { + return bodyData.replace("GMT+0000 (Coordinated Universal Time)", "(UTC)"); + }; + + const sortComments = (bodyData) => { + const bodyArray = bodyData.split("\n").filter((elem) => elem.includes("Deployment")); + let commentBody = ``; + const timeArray = []; + const timeObj = {}; + bodyArray.forEach((element) => { + const timestamp = new Date( + element + .match(/Deployment:.*\(UTC\)/)[0] + .replace("Deployment:", "") + .trim() + ).getTime(); + timeArray.push(timestamp); + timeObj[timestamp] = element; + }); + const timeSortArray = timeArray.sort(); + + timeSortArray.forEach((em) => { + commentBody = commentBody + `${timeObj[em]}\n`; + }); + return commentBody; + }; + + const createNewCommitComment = async (body = defaultBody) => { + body = GMTConverter(body); + verifyInput(body) && + (await github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: commitSha, + body: body, + })); + }; + + const createNewPRComment = async (body = defaultBody) => { + body = GMTConverter(body); + verifyInput(body) && + (await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullRequestNumber, + body: body, + })); + }; + + const editExistingPRComment = async () => { + const { body: botBody, id: commentId } = botCommentsArray[0]; + let commentBody = `${GMTConverter(defaultBody)}\n` + `${GMTConverter(botBody)}`; + verifyInput(commentBody) && + (await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body: commentBody, + })); + }; + + const deleteExistingPRComments = async () => { + const delPromises = botCommentsArray.map(async (elem) => { + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: elem.id, + }); + }); + await Promise.all(delPromises); + }; + + const mergeExistingPRComments = async () => { + let commentBody = `${GMTConverter(defaultBody)}\n`; + botCommentsArray.forEach(({ body }) => { + commentBody = commentBody + `${GMTConverter(body)}\n`; + }); + await createNewPRComment(commentBody); + await deleteExistingPRComments(); + }; + + const processPRComments = async () => { + const perPage = 30; + let pageNumber = 1; + let hasMore = true; + const commentsArray = []; + + while (hasMore) { + const { data: issueComments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullRequestNumber, + per_page: perPage, + page: pageNumber, + }); + pageNumber++; + + if (issueComments.length > 0) { + commentsArray.push(...issueComments); + } else { + hasMore = false; + } + } + + if (commentsArray.length > 0) { + commentsArray.forEach((elem) => { + if (elem.user.type === "Bot" && elem.user.login === "ubiquibot[bot]") { + botCommentsArray.push(elem); + } + }); + const botLen = botCommentsArray.length; + switch (botLen) { + case 0: + //no (bot) comments + createNewPRComment(); + break; + case 1: + //single (bot) comment [] + editExistingPRComment(); + break; + default: + //multiple (bot) comments [] + mergeExistingPRComments(); + break; + } + } else { + //no comments (user|bot) [] + createNewPRComment(); + } + }; + + if (eventName == "pull_request") { + console.log("Creating a comment for the pull request"); + await processPRComments(); + } else { + console.log("Creating a comment for the commit"); + await createNewCommitComment(); + } + }; + \ No newline at end of file