UNPKG

@wellsite/version-generator

Version:
417 lines 18.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultExecutor = void 0; exports.getLatestTag = getLatestTag; exports.getCommitCountFromGitHub = getCommitCountFromGitHub; exports.getCommitCount = getCommitCount; exports.getCurrentBranch = getCurrentBranch; exports.getShortCommitHash = getShortCommitHash; exports.cleanBranchName = cleanBranchName; exports.generatePackageVersion = generatePackageVersion; exports.writeVersionToFile = writeVersionToFile; exports.generateAndWriteVersion = generateAndWriteVersion; const child_process_1 = require("child_process"); const android_version_1 = require("./android-version"); const ios_version_1 = require("./ios-version"); const fs_1 = require("fs"); const path_1 = require("path"); const https = __importStar(require("https")); /** * Default executor implementation using real commands */ exports.defaultExecutor = { execCommand: (command, cwd) => (0, child_process_1.execSync)(command, cwd ? { cwd } : undefined) .toString() .trim(), fileExists: (filePath) => (0, fs_1.existsSync)(filePath), readFile: (filePath) => (0, fs_1.readFileSync)(filePath, 'utf8'), writeFile: (filePath, content) => (0, fs_1.writeFileSync)(filePath, content), mkdirSync: (dirPath, options) => (0, fs_1.mkdirSync)(dirPath, options), getGitHubData: async (url, env = process.env) => { // Fail if GITHUB_TOKEN is missing when running in GitHub Actions if (!env.GITHUB_TOKEN && env.GITHUB_ACTIONS === 'true') { throw new Error('GITHUB_TOKEN environment variable is not set. This is required for GitHub API access when running in GitHub Actions.'); } // Warn if GITHUB_TOKEN is missing but not in GitHub Actions if (!env.GITHUB_TOKEN && env.GITHUB_ACTIONS !== 'true') { console.warn('Warning: GITHUB_TOKEN environment variable is not set. GitHub API requests may be rate-limited.'); } return new Promise((resolve, reject) => { const req = https.get(url, { headers: Object.assign({ 'User-Agent': 'Node.js', Accept: 'application/vnd.github.v3+json' }, (env.GITHUB_TOKEN ? { Authorization: `token ${env.GITHUB_TOKEN}` } : {})), }, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(e); } }); }); req.on('error', (e) => { reject(e); }); req.end(); }); }, }; /** * Gets the latest tag from GitHub API * * @param executor - Custom executor for dependency injection * @param env - Environment variables for dependency injection * @returns Promise resolving to the latest tag from GitHub API */ async function getLatestTagFromGitHub(executor = exports.defaultExecutor, env = process.env) { var _a; const owner = env.GITHUB_REPOSITORY_OWNER; const repo = (_a = env.GITHUB_REPOSITORY) === null || _a === void 0 ? void 0 : _a.split('/')[1]; if (!owner || !repo) { throw new Error('Missing required GitHub environment variables'); } const url = `https://api.github.com/repos/${owner}/${repo}/tags`; try { const response = await executor.getGitHubData(url); if (!Array.isArray(response) || response.length === 0) { throw new Error('No tags found in repository'); } // First find all tags that start with 'v' const allVTags = response.filter((tag) => typeof tag.name === 'string' && tag.name.startsWith('v')); if (allVTags.length === 0) { throw new Error('No tags starting with "v" found in repository'); } // Then filter to only include tags that match the format vX.Y (e.g., v1.2) // This ensures we only consider proper version tags and ignore major version references like v1 const versionTagRegex = /^v\d+\.\d+$/; const vTags = allVTags.filter((tag) => versionTagRegex.test(tag.name)); if (vTags.length === 0) { throw new Error('No tags matching the required format vX.Y found in repository'); } return vTags[0].name; } catch (error) { throw new Error(`Failed to get latest tag from GitHub: ${error instanceof Error ? error.message : String(error)}`); } } /** * Gets the latest tag from git * * @param options - Options for getting the latest tag * @param options.executor - Custom executor for dependency injection * @param options.env - Environment variables for dependency injection * @returns Promise resolving to the latest tag */ async function getLatestTag(options = {}) { const executor = options.executor || exports.defaultExecutor; const env = options.env || process.env; // If running in GitHub Actions, use the GitHub API if (env.GITHUB_ACTIONS === 'true') { try { return await getLatestTagFromGitHub(executor, env); } catch (error) { throw new Error(`Failed to get tag from GitHub API: ${error}`); } } try { // Get tags matching v*.* pattern in our ancestry and sort by creation date (newest first) // This will find tags like v1.2, v2.3, etc. but not v1, v2, etc. const gitCommand = 'git tag --list "v*.*" --sort=-creatordate --merged HEAD'; const vTags = executor.execCommand(gitCommand, options.cwd).split('\n').filter(Boolean); if (vTags.length === 0) { throw new Error('No tags matching v*.* pattern found in repository ancestry'); } // Further filter tags to match the exact vX.Y format (e.g., v1.2) const versionTagRegex = /^v\d+\.\d+$/; const versionTags = vTags.filter((tag) => versionTagRegex.test(tag.trim())); // No need to sort as Git already sorted by creation date (newest first) if (versionTags.length === 0) { throw new Error('No tags matching the required format vX.Y found in repository ancestry'); } return versionTags[0]; } catch (error) { throw new Error(`No git tags found ${error instanceof Error ? error.message : String(error)}`); } } /** * Gets the commit count from GitHub API * * @param fromTag - The tag to count commits from * @param executor - Custom executor for dependency injection * @param env * @returns Promise resolving to the commit count */ async function getCommitCountFromGitHub(fromTag, executor = exports.defaultExecutor, env = process.env) { var _a; const owner = env.GITHUB_REPOSITORY_OWNER; const repo = (_a = env.GITHUB_REPOSITORY) === null || _a === void 0 ? void 0 : _a.split('/')[1]; const sha = env.GITHUB_SHA; if (!owner || !repo || !sha) { throw new Error('Missing required GitHub environment variables'); } const url = `https://api.github.com/repos/${owner}/${repo}/compare/${fromTag}...${sha}`; try { const response = await executor.getGitHubData(url); const aheadBy = response.ahead_by; if (typeof aheadBy !== 'number') { throw new Error(`Invalid response from GitHub API: ahead_by is not a number: ${aheadBy}`); } return aheadBy; } catch (error) { throw new Error(`Failed to get commit count from GitHub: ${error instanceof Error ? error.message : String(error)}`); } } /** * Gets the commit count from git * * @param fromTag - The tag to count commits from * @param options - Options for getting commit count * @param options.executor - Custom executor for dependency injection * @returns Promise resolving to the commit count */ async function getCommitCount(fromTag, options = {}) { const executor = options.executor || exports.defaultExecutor; const env = options.env || process.env; // No special case needed as we're failing when no tags are found if (env.GITHUB_ACTIONS === 'true') { try { return await getCommitCountFromGitHub(fromTag, executor, env); } catch (error) { throw new Error(`Failed to get commit count from tag ${fromTag} via \ GITHUB API: ${error instanceof Error ? error.message : String(error)}`); } } try { const gitCommand = `git rev-list ${fromTag}..HEAD --count`; const count = executor.execCommand(gitCommand, options.cwd); return parseInt(count, 10); } catch (error) { throw new Error(`Failed to get commit count from tag ${fromTag}`); } } /** * Gets the current branch name * * @param options - Options for getting the current branch * @param options.executor - Custom executor for dependency injection * @returns The current branch name */ function getCurrentBranch(options = {}) { const executor = options.executor || exports.defaultExecutor; const env = options.env || process.env; // GITHUB_HEAD_REF is set when the workflow is triggered by a pull request // In this case, GITHUB_REF_NAME is incorrect (eg: refs/pull/42/merge) if (env.GITHUB_HEAD_REF) { return env.GITHUB_HEAD_REF; } if (env.GITHUB_REF_NAME) { return env.GITHUB_REF_NAME; } try { const gitCommand = 'git rev-parse --abbrev-ref HEAD'; return executor.execCommand(gitCommand, options.cwd); } catch (error) { throw new Error('Failed to get current branch'); } } /** * Gets the short commit hash * * @param options - Options for getting the short commit hash * @param options.executor - Custom executor for dependency injection * @returns The short commit hash */ function getShortCommitHash(options = {}) { const executor = options.executor || exports.defaultExecutor; const env = options.env || process.env; if (env.GITHUB_SHA) { return env.GITHUB_SHA.substring(0, 8); } try { const gitCommand = 'git rev-parse --short=8 HEAD'; return executor.execCommand(gitCommand, options.cwd); } catch (error) { throw new Error('Failed to get commit hash'); } } /** * Cleans a branch name for use in a version string * Removes refs/heads/ or refs/pull/ prefixes if present * * @param branchName - The branch name to clean * @returns The cleaned branch name */ function cleanBranchName(branchName) { // Remove refs/heads/ or refs/pull/ prefixes if present branchName = branchName.replace(/^refs\/(heads|pull)\//, ''); // Replace non-alphanumeric characters with hyphens return branchName.replace(/[^a-zA-Z0-9]/g, '-'); } /** * Generates version information based on git information * * @param dir - Optional directory for command execution * @param options - Options for version generation * @param options.executor - Custom executor for dependency injection * @param options.env - Environment variables for dependency injection * @param options.android - Android version options * @returns Promise resolving to the version information object */ async function generatePackageVersion(dir, options = {}) { var _a, _b; const executor = options.executor || exports.defaultExecutor; const env = options.env || process.env; // Get the latest tag (format: v<major>.<minor>) const tag = await getLatestTag({ executor, env, cwd: dir }); if (!tag.startsWith('v')) { throw new Error('Tag must start with "v"'); } // Extract major and minor from tag const [major, minor] = tag.substring(1).split('.'); if (!major || !minor) { throw new Error('Invalid tag format. Expected v<major>.<minor>'); } // Get patch (number of commits since tag) const patch = await getCommitCount(tag, { executor, env, cwd: dir }); // Get branch name and clean it const branchName = cleanBranchName(getCurrentBranch({ executor, env, cwd: dir })); // Get short commit hash const commitHash = getShortCommitHash({ executor, env, cwd: dir }); // Generate version string in npm semver compatible format const version = `${major}.${minor}.${patch}-${branchName}.${commitHash}`; // Generate app release version (only major.minor.patch) const appReleaseVersion = `${major}.${minor}.${patch}`; // Get Android version code if Android options are provided let androidVersionCode; if ((_a = options.android) === null || _a === void 0 ? void 0 : _a.enabled) { // Since Android option is explicitly enabled, we should fail if we can't generate the version code const versionCode = await (0, android_version_1.getAndroidVersionCode)(Object.assign(Object.assign({}, options.android), { currentMajorVersion: parseInt(major, 10) })); // Only set androidVersionCode if a valid version code was returned if (versionCode > 0) { androidVersionCode = versionCode; } else { throw new Error('Android version code generation failed: returned invalid version code'); } } // Get iOS build number if iOS options are provided let iosBuildNumber; let iosBuildNumberString; if ((_b = options.ios) === null || _b === void 0 ? void 0 : _b.enabled) { // Since iOS option is explicitly enabled, we should fail if we can't generate the build number const buildNumberInfo = await (0, ios_version_1.getIosBuildNumberInfo)(Object.assign(Object.assign({}, options.ios), { appReleaseVersion, commitHash })); //ios build number is a monotomically increasing value for the specific shortVersionString iosBuildNumber = buildNumberInfo.buildNumber; //This is what we end up setting in the IPA. it's the combination of the buildNumber and the commit hash. iosBuildNumberString = buildNumberInfo.buildVersion; // Ensure we were able to retrieve a build number, otherwise we fail. if (iosBuildNumber <= 0) { throw new Error('iOS build number generation failed: returned invalid build number'); } } // Return the complete version info object return { major, minor, patch, branchName, commitHash, version, appReleaseVersion, androidVersionCode, iosBuildNumber, iosBuildNumberString, }; } /** * Writes the generated version to a file * * @param versionInfo - The version information to write * @param filePath - The path to write the version file to * @param options - Options for writing the version file * @param options.executor - Custom executor for dependency injection * @param options.env - Environment variables for dependency injection */ function writeVersionToFile(versionInfo, filePath, options = {}) { const executor = options.executor || exports.defaultExecutor; // We don't currently use env in this function, but include it for consistency const fileContent = JSON.stringify(versionInfo, null, 2); // Create directory if it doesn't exist executor.mkdirSync((0, path_1.dirname)(filePath), { recursive: true }); executor.writeFile(filePath, fileContent); } /** * Generates version information and optionally writes it to the specified output file as a JSON object * * @param dir - The directory for command execution and relative path resolution (defaults to current working directory) * @param outputFilePath - Optional output file path (relative to dir if not absolute) where the version file should be written * @param options - Options for generating and writing the version * @param options.executor - Custom executor for dependency injection * @param options.env - Environment variables for dependency injection * @param options.android - Android version options * @returns Promise resolving to the version information object */ async function generateAndWriteVersion(dir, outputFilePath, options = {}) { const executor = options.executor || exports.defaultExecutor; const env = options.env || process.env; const versionInfo = await generatePackageVersion(dir, { executor, env, android: options.android, ios: options.ios, }); // If an output file path is provided, write the version to that location as a JSON object if (outputFilePath) { // If outputFilePath is an absolute path, use it directly, otherwise join with dir const versionFilePath = outputFilePath.startsWith('/') ? outputFilePath : (0, path_1.join)(dir, outputFilePath); // Write the version file writeVersionToFile(versionInfo, versionFilePath, { executor }); } return versionInfo; } //# sourceMappingURL=index.js.map