const { ScreepsAPI } = require("screeps-api"); const core = require("@actions/core"); const fs = require("fs"); const glob = require("glob"); const path = require("path"); /** * Replaces specific placeholder strings within the provided content with corresponding dynamic values. * * This function specifically targets three placeholders: * - {{gitHash}} is replaced with the current Git commit hash, obtained from the GITHUB_SHA environment variable. * - {{gitRef}} is replaced with the Git reference (branch or tag) that triggered the workflow, obtained from the GITHUB_REF environment variable. * - {{deployTime}} is replaced with the current ISO timestamp. * * Note: This function is designed for use within a GitHub Actions workflow where GITHUB_SHA and GITHUB_REF environment variables are automatically set. * * @param {string} content - The string content in which placeholders are to be replaced. * @returns {string} The content with placeholders replaced by their respective dynamic values. */ function replacePlaceholders(content) { const deployTime = new Date().toISOString(); return content .replace(/{{gitHash}}/g, process.env.GITHUB_SHA) .replace(/{{gitRef}}/g, process.env.GITHUB_REF) .replace(/{{deployTime}}/g, deployTime); } /** * Reads all files matching a specified pattern, replaces certain placeholders in their content, and writes the updated content back to the files. * * This function searches for files in the filesystem using the provided glob pattern, optionally prefixed. It reads each file, * uses the `replacePlaceholders` function to replace specific placeholders in the file's content, and then writes the modified content * back to the original file. This is useful for dynamically updating file contents in a batch process, such as during a build or deployment. * * @param {string} pattern - The glob pattern used to find files. Example: '*.js' for all JavaScript files. * @param {string} [prefix] - An optional directory prefix to prepend to the glob pattern. This allows searching within a specific directory. * @returns {Promise} A promise that resolves with an array of file paths that were processed, or rejects with an error if the process fails. */ async function readReplaceAndWriteFiles(pattern, prefix) { return new Promise((resolve, reject) => { const globPattern = prefix ? path.join(prefix, pattern) : pattern; glob(globPattern, async (err, files) => { if (err) { return reject(err); } let processPromises = []; files.forEach((file) => { let processPromise = fs.promises .readFile(file, "utf8") .then((content) => { content = replacePlaceholders(content, replacements); return fs.promises.writeFile(file, content); }); processPromises.push(processPromise); }); try { await Promise.all(processPromises); resolve(files); } catch (processError) { reject(processError); } }); }); } /** * Reads files matching a glob pattern into a dictionary. * @param {string} pattern - Glob pattern to match files. * @param {string} prefix - Directory prefix for file paths. * @returns {Promise} - Promise resolving to a dictionary of file contents keyed by filenames. */ function readFilesIntoDict(pattern, prefix) { // Prepend the prefix to the glob pattern const globPattern = prefix ? path.join(prefix, pattern) : pattern; return new Promise((resolve, reject) => { glob(globPattern, (err, files) => { if (err) { return reject(err); } let fileDict = {}; let readPromises = []; files.forEach((file) => { let readPromise = fs.promises.readFile(file, "utf8").then((content) => { // Remove the prefix from the filename and drop the file suffix let key = file; if (prefix && file.startsWith(prefix)) { key = key.slice(prefix.length); } key = path.basename(key, path.extname(key)); // Drop the file suffix fileDict[key] = content; }); readPromises.push(readPromise); }); // Use Promise.all to ensure all files are read before resolving Promise.all(readPromises) .then(() => resolve(fileDict)) .catch(reject); }); }); } /** * Validates the provided authentication credentials. * @param {string} token - The authentication token. * @param {string} username - The username. * @param {string} password - The password. * @returns {string|null} - Returns an error message if validation fails, otherwise null. */ function validateAuthentication(token, username, password) { if (token) { if (username || password) { return "Token is defined along with username and/or password."; } } else { if (!username && !password) { return "Neither token nor password and username are defined."; } if (username && !password) { return "Username is defined but no password is provided."; } if (!username && password) { return "Password is defined but no username is provided."; } } return null; // No errors found } /** * Posts code to Screeps server. */ async function postCode() { const protocol = core.getInput("protocol") || "https"; const hostname = core.getInput("hostname") || "screeps.com"; const port = core.getInput("port") || "443"; const path = core.getInput("path") || "/"; const token = core.getInput("token") || undefined; const username = core.getInput("username") || undefined; const password = core.getInput("password") || undefined; const prefix = core.getInput("source-prefix"); const pattern = core.getInput("pattern") || "*.js"; const branch = core.getInput("branch") || "default"; const gitReplace = core.getInput("git-replace") || null; if (gitReplace) { await readReplaceAndWriteFiles(gitReplace, prefix); } const files_to_push = await readFilesIntoDict(pattern, prefix); core.info(`Trying to upload the following files to ${branch}:`); Object.keys(files_to_push).forEach((key) => { core.info(`Key: ${key}`); }); const login_arguments = { token: token, username: username, password: password, protocol: protocol, hostname: hostname, port: port, path: path, }; core.info("login_arguments:"); core.info(JSON.stringify(login_arguments, null, 2)); const errorMessage = validateAuthentication(token, username, password); if (errorMessage) { core.error(errorMessage); return; } let api = new ScreepsAPI(login_arguments); if (token) { const response = await api.code.set(branch, files_to_push); core.info(JSON.stringify(response, null, 2)); console.log(`Code set successfully to ${branch}`); } else { let response; core.info(`Logging in as user ${username}`); response = await Promise.resolve() .then(() => api.auth(username, password, login_arguments)) .then(() => { api.code.set(branch, files_to_push); }) .then(() => { console.log(`Code set successfully to ${branch}`); }) .catch((err) => { console.error("Error:", err); }); } } postCode();