Skip to content

Commit

Permalink
adding new helper files for the updated process
Browse files Browse the repository at this point in the history
  • Loading branch information
TaylorFries committed Jan 19, 2024
1 parent 11756ee commit 0a30be4
Show file tree
Hide file tree
Showing 4 changed files with 545 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .github/helpers/create-npm-dep-report-issues.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const path = require("path");
const createAndCloseExistingIssue = require("./github-api/create-and-close-existing-issue");
const outputText = require(path.resolve(__dirname, `../../outputText.json`));

/**
* THIS FILE DOES NOT REQUIRE ANY EDITING.
* Place within .github/helpers/
*/

// Get package.json paths from env.
const packageJsonPaths = JSON.parse(process.env.packageJsonPaths);

(async () => {
// Create an array of promises for each packageJsonPath.
const promises = packageJsonPaths.map(async (packagePath) => {
// Await the completion of create and close existing issue.
await createAndCloseExistingIssue(packagePath, outputText[packagePath]);
});

// Wait for all issues to be created.
await Promise.all(promises);
})();
261 changes: 261 additions & 0 deletions .github/helpers/create-npm-dep-report.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
const path = require("path");
const outdatedDeps = require(path.resolve(
__dirname,
`../../outdatedDeps.json`
));

const LOCAL_TEST = false;
const TEST_PACKAGEJSON_PATHS = ["src/frontend", "src/backend"];

/**
* THIS FILE DOES NOT REQUIRE ANY EDITING.
* Place within .github/helpers/
*
* To test this file locally,
* - Generate output from parse-npm-deps.js
* - Set LOCAL_TEST variable to true.
* - Edit TEST_PACKAGEJSON_PATHS if necessary.
* - From root, run "node .github/helpers/create-npm-dep-report > outputText.json"
* - Check the outputText.json file, then delete it.
*/

// Get package.json paths from env.
const packageJsonPaths = LOCAL_TEST
? TEST_PACKAGEJSON_PATHS
: JSON.parse(process.env.packageJsonPaths);

// Save results to json.
let results = {};

// Emojis.
const check = "✔️";
const attention = "⚠️";

// Badge color codes (checked for WCAG standards).
const red = "701807"; // White text.
const orange = "9e3302"; // White text.
const yellow = "f5c60c"; // Black text.
const green = "0B6018"; // White text.
const blue = "0859A1"; // White text.

// GitHub Markdown Formatting.
const heading = (text, size) => `${"#".repeat(size)} ${text}\n`;
const codeBlock = (text, language) => `\`\`\`${language}\n${text}\n\`\`\`\n\n`;
const lineBreak = () => `\n<br />\n`;
const line = (text) => `${text}\n`;

// Formatted date.
const getFormattedDate = () => {
const date = new Date();

// Get day of the month.
const day = date.getDate();

// Determine the ordinal suffix.
const ordinal = (day) => {
const s = ["th", "st", "nd", "rd"];
const v = day % 100;
return day + (s[(v - 20) % 10] || s[v] || s[0]);
};

// Formatter for the rest of the date.
const formatter = new Intl.DateTimeFormat("en-US", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});

// Format the date and replace the day number with ordinal.
return formatter.format(date).replace(/\d+/, ordinal(day));
};

// Messages.
const title = `NPM Dependency Report - ${getFormattedDate()}`;
const subTitle =
"Versions of npm packages have been checked against their latest versions from the npm registry.";
const upToDateMsg = "dependencies are all up-to-date.";
const outOfDateMsg = "dependencies are out-of-date.";

// Calculate percentage of packages up to date.
const calculateUpToDatePercentage = (total, outdated) => {
if (total === 0) return 0;

const upToDatePackages = total - outdated;
const percentage = (upToDatePackages / total) * 100;
return Math.round(percentage);
};

// Output a command to install all dependencies in array.
const outputMultiPackageInstallCmd = (dependencies, packagePath, isDevDep) => {
let installCmd = `npm install${isDevDep ? " -D" : ""} `;
installCmd += dependencies
.map((obj) => `${obj.dependency}@${obj.latestVersion}`)
.join(" ");

results[packagePath] += `${codeBlock(installCmd, "")}\n`;
};

// Output Dependencies in an array.
const outputDepsByVersionChange = (
dependencies,
versionChange,
packagePath,
isDevDep
) => {
const headerTag = isDevDep ? `${versionChange}_dev` : `${versionChange}`;
const badgeColor =
versionChange === "major"
? orange
: versionChange === "minor"
? blue
: green;

// Output header.
results[packagePath] += `${line(`![${headerTag}]`)}\n\n`;

// Output start of spoiler.
results[packagePath] += `${line(`<details>`)}\n`;
results[packagePath] += `${line(`<summary>`)}`;
results[packagePath] += `${line(
`Expand to see individual installs. <br /><br />\n`
)}`;

// Output a command to install all dependencies in array.
outputMultiPackageInstallCmd(dependencies, packagePath, isDevDep);

// Output end of spoiler summary.
results[packagePath] += `${line(`</summary>\n`)}`;

// List dependency updates.
for (const key in dependencies) {
const { dependency, version, latestVersion } = dependencies[key];

results[packagePath] += `${line(
`- [ ] \`${dependency}\` Update from version \`${version}\` to \`${latestVersion}\` by running...`
)}`;
results[packagePath] += `${codeBlock(
`npm install${isDevDep ? " -D" : ""} ${dependency}@${latestVersion}`,
""
)}`;
}

// Output end of spoiler.
results[packagePath] += `${line(`</details>\n`)}`;

// Add Header text
results[packagePath] += `${line(
`[${headerTag}]: https://img.shields.io/badge/${versionChange}_updates_(${dependencies.length})-${badgeColor}?style=for-the-badge \n`
)}`;

results[packagePath] += `${lineBreak()}\n`;
};

// Output dependencies that need updating.
const outputDeps = (dependenciesObj, packagePath, isDevDep) => {
// Return if no dependencies to update.
if (dependenciesObj.outdated <= 0) return;

// Output title.
results[packagePath] += `${lineBreak()}\n`;
if (isDevDep)
results[packagePath] += `${heading(
"Development Dependencies to Update:",
3
)}`;
else
results[packagePath] += `${heading(
"Production Dependencies to Update:",
3
)}`;

// Output MAJOR depedencies to update.
const major = dependenciesObj.major;
if (major.length > 0)
outputDepsByVersionChange(major, "major", packagePath, isDevDep);

// Output MINOR depedencies to update.
const minor = dependenciesObj.minor;
if (minor.length > 0)
outputDepsByVersionChange(minor, "minor", packagePath, isDevDep);

// Output PATCH depedencies to update.
const patch = dependenciesObj.patch;
if (patch.length > 0)
outputDepsByVersionChange(patch, "patch", packagePath, isDevDep);
};

// Escape special characters for GitHub Actions.
const escapeForGitHubActions = (str) =>
str.replace(/%/g, "%25").replace(/\n/g, "%0A").replace(/\r/g, "%0D");

(async () => {
// Create an array of promises for each packageJsonPath.
const promises = packageJsonPaths.map(async (packagePath) => {
results[packagePath] = "";

// Read the outdatedDeps file and get dependencies and devDependencies.
const deps = outdatedDeps[packagePath].deps ?? {};
const devDeps = outdatedDeps[packagePath].devDeps ?? {};

// Output title.
results[packagePath] += `${heading(title, 2)}`;
results[packagePath] += `${line(subTitle)}\n`;

// Get percentage of packages up to date.
const percentageUpToDate = calculateUpToDatePercentage(
deps.total + devDeps.total,
deps.outdated + devDeps.outdated
);

let percentageColor = green;
if (percentageUpToDate <= 50) percentageColor = red;
else if (percentageUpToDate <= 70) percentageColor = orange;
else if (percentageUpToDate <= 90) percentageColor = yellow;

// Output percentage.
results[packagePath] += `${line("![COVERAGE_PERCENTAGE]\n")}`;
results[packagePath] += `${line(
`\n[COVERAGE_PERCENTAGE]: https://img.shields.io/badge/percentage_of_dependencies_up_to_date-${percentageUpToDate}-${percentageColor}?style=for-the-badge \n\n`
)}`;

// Output summary.
if (deps.outdated === 0)
results[packagePath] += `${line(`${check} - Production ${upToDateMsg}`)}`;
else
results[packagePath] += `${line(
`${attention} - ${deps.outdated} Production ${outOfDateMsg}`
)}`;

if (devDeps.outdated === 0)
results[packagePath] += `${line(
`${check} - Development ${upToDateMsg}`
)}`;
else
results[packagePath] += `${line(
`${attention} - ${devDeps.outdated} Development ${outOfDateMsg}`
)}`;

// Output reminder.
if (deps.outdated > 0 || devDeps.outdated > 0) {
results[packagePath] += `${line(
`\n**Make sure to change directory to where the package.json is located using...**`
)}`;
results[packagePath] += `${codeBlock(`cd ${packagePath}`, "")}`;
}

// Await the completion of output for both dependencies and devDependencies.
await Promise.all([
outputDeps(deps, packagePath, false),
outputDeps(devDeps, packagePath, true),
]);

results[packagePath] = escapeForGitHubActions(results[packagePath]);
});

// Wait for all outputs to complete.
await Promise.all(promises);

// Once all promises are resolved, log the results.
console.log(JSON.stringify(results, null, 2));
})();
72 changes: 72 additions & 0 deletions .github/helpers/parse-json5-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const fs = require("fs");
const json5 = require("json5");

/**
* THIS FILE DOES NOT REQUIRE ANY EDITING.
* Place within .github/helpers/
*/

// Check if a file path is provided.
if (process.argv.length < 3) {
console.log("Usage: node parse-json5-config <path_to_json5_file>");
process.exit(1);
}
const filePath = process.argv[2];

/**
* Read a Json5 file and parse out its values to a github workflow as output vars.
*
* Usage in GitHub Workflow:
*
* jobs:
* # Parse Output Vars from config.
* parse-json5-config:
* runs-on: ubuntu-22.04
* outputs:
varFromConfig: ${{ steps.parse_config.outputs.varFromConfig }}
*
* steps:
* # Checkout branch.
* - name: Checkout Repository
* uses: actions/checkout@v4
*
* # Install json5 npm package for parsing config.
* - name: Install Dependencies
* run: npm install json5
*
* # Run script to convert json5 config to output vars.
* - name: Run Script
* id: parse_config
* run: node .github/helpers/parse-json5-config <path-to-json5-file>
*
* # Another job...
* another-job:
* runs-on: ubuntu-22.04
* needs: parse-json5-config
* env:
* varFromConfig: ${{ needs.parse-json5-config.outputs.varFromConfig }}
*
*/
fs.readFile(filePath, "utf8", (err, data) => {
if (err) {
console.error("Error reading the file:", err);
return;
}

try {
// Parse the JSON5 data
const jsonData = json5.parse(data);

// Set each key-value pair as an environment variable
for (const [key, value] of Object.entries(jsonData)) {
// Serialize arrays and objects to JSON strings
const envValue =
typeof value === "object" ? JSON.stringify(value) : value;

// Output each key-value pair for GitHub Actions
console.log(`::set-output name=${key}::${envValue}`);
}
} catch (parseError) {
console.error("Error parsing JSON5:", parseError);
}
});
Loading

0 comments on commit 0a30be4

Please sign in to comment.