|
| 1 | +import * as core from "@actions/core"; |
| 2 | +import { Octokit } from "@octokit/rest"; |
| 3 | +import simpleGit from "simple-git"; |
| 4 | + |
| 5 | +const token = process.env.GITHUB_TOKEN; |
| 6 | +const [owner, repo] = process.env.GITHUB_REPOSITORY?.split("/") || []; |
| 7 | +const pullNumber = process.env.GITHUB_PR_NUMBER || process.env.PULL_REQUEST_NUMBER || "0"; |
| 8 | +const baseRef = process.env.GITHUB_BASE_REF; |
| 9 | +const excludedFiles: string[] = process.env.EXCLUDED_FILES |
| 10 | + ? process.env.EXCLUDED_FILES.split("\n") |
| 11 | + .map((file) => file.trim()) |
| 12 | + .filter(Boolean) |
| 13 | + : []; |
| 14 | + |
| 15 | +if (!token || !owner || !repo || pullNumber === "0" || !baseRef) { |
| 16 | + core.setFailed("Missing required environment variables."); |
| 17 | + process.exit(1); |
| 18 | +} |
| 19 | + |
| 20 | +const octokit = new Octokit({ auth: token }); |
| 21 | +const git = simpleGit(); |
| 22 | + |
| 23 | +async function main() { |
| 24 | + try { |
| 25 | + const { data: pullRequest } = await octokit.pulls.get({ |
| 26 | + owner, |
| 27 | + repo, |
| 28 | + pull_number: parseInt(pullNumber, 10), |
| 29 | + }); |
| 30 | + |
| 31 | + const baseSha = pullRequest.base.sha; |
| 32 | + const headSha = pullRequest.head.sha; |
| 33 | + |
| 34 | + await git.fetch(["origin", baseSha, headSha]); |
| 35 | + |
| 36 | + const diff = await git.diff([`${baseSha}...${headSha}`]); |
| 37 | + |
| 38 | + core.info("Checking for empty strings..."); |
| 39 | + const violations = parseDiffForEmptyStrings(diff); |
| 40 | + |
| 41 | + if (violations.length > 0) { |
| 42 | + violations.forEach(({ file, line, content }) => { |
| 43 | + core.warning( |
| 44 | + "Detected an empty string.\n\nIf this is during variable initialization, consider using a different approach.\nFor more information, visit: https://www.github.com/ubiquity/ts-template/issues/31", |
| 45 | + { |
| 46 | + file, |
| 47 | + startLine: line, |
| 48 | + } |
| 49 | + ); |
| 50 | + }); |
| 51 | + |
| 52 | + // core.setFailed(`${violations.length} empty string${violations.length > 1 ? "s" : ""} detected in the code.`); |
| 53 | + |
| 54 | + await octokit.rest.checks.create({ |
| 55 | + owner, |
| 56 | + repo, |
| 57 | + name: "Empty String Check", |
| 58 | + head_sha: headSha, |
| 59 | + status: "completed", |
| 60 | + conclusion: violations.length > 0 ? "failure" : "success", |
| 61 | + output: { |
| 62 | + title: "Empty String Check Results", |
| 63 | + summary: `Found ${violations.length} violation${violations.length !== 1 ? "s" : ""}`, |
| 64 | + annotations: violations.map((v) => ({ |
| 65 | + path: v.file, |
| 66 | + start_line: v.line, |
| 67 | + end_line: v.line, |
| 68 | + annotation_level: "warning", |
| 69 | + message: "Empty string found", |
| 70 | + raw_details: v.content, |
| 71 | + })), |
| 72 | + }, |
| 73 | + }); |
| 74 | + } else { |
| 75 | + core.info("No empty strings found."); |
| 76 | + } |
| 77 | + } catch (error) { |
| 78 | + core.setFailed(`An error occurred: ${error instanceof Error ? error.message : String(error)}`); |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +function parseDiffForEmptyStrings(diff: string) { |
| 83 | + const violations: Array<{ file: string; line: number; content: string }> = []; |
| 84 | + const diffLines = diff.split("\n"); |
| 85 | + |
| 86 | + let currentFile: string; |
| 87 | + let headLine = 0; |
| 88 | + let inHunk = false; |
| 89 | + |
| 90 | + diffLines.forEach((line) => { |
| 91 | + const hunkHeaderMatch = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line); |
| 92 | + if (hunkHeaderMatch) { |
| 93 | + headLine = parseInt(hunkHeaderMatch[1], 10); |
| 94 | + inHunk = true; |
| 95 | + return; |
| 96 | + } |
| 97 | + |
| 98 | + if (line.startsWith("--- a/") || line.startsWith("+++ b/")) { |
| 99 | + currentFile = line.slice(6); |
| 100 | + inHunk = false; |
| 101 | + return; |
| 102 | + } |
| 103 | + |
| 104 | + // Skip files in excludedFiles |
| 105 | + if (excludedFiles.includes(currentFile)) { |
| 106 | + return; |
| 107 | + } |
| 108 | + |
| 109 | + // Only process TypeScript files |
| 110 | + if (!currentFile?.endsWith(".ts")) { |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + if (inHunk && line.startsWith("+")) { |
| 115 | + // Check for empty strings in TypeScript syntax |
| 116 | + if (/^\+.*""/.test(line)) { |
| 117 | + // Ignore empty strings in comments |
| 118 | + if (!line.trim().startsWith("//") && !line.trim().startsWith("*")) { |
| 119 | + // Ignore empty strings in template literals |
| 120 | + if (!/`[^`]*\$\{[^}]*\}[^`]*`/.test(line)) { |
| 121 | + violations.push({ |
| 122 | + file: currentFile, |
| 123 | + line: headLine, |
| 124 | + content: line.substring(1).trim(), |
| 125 | + }); |
| 126 | + } |
| 127 | + } |
| 128 | + } |
| 129 | + headLine++; |
| 130 | + } else if (!line.startsWith("-")) { |
| 131 | + headLine++; |
| 132 | + } |
| 133 | + }); |
| 134 | + |
| 135 | + return violations; |
| 136 | +} |
| 137 | +main().catch((error) => { |
| 138 | + core.setFailed(`Error running empty string check: ${error instanceof Error ? error.message : String(error)}`); |
| 139 | +}); |
0 commit comments