UNPKG

@wellsite/version-generator

Version:
443 lines 19.5 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.getCommitCountFromGitHub = getCommitCountFromGitHub; exports.getLatestTag = getLatestTag; 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")); const normalize_env_1 = require("./normalize-env"); /** * 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) => { // Fail if TOKEN is missing when running in CI if (!env.TOKEN && env.CI_ENV === 'true') { throw new Error('TOKEN environment variable is not set. This is required for GitHub API access when running in CI.'); } // Warn if TOKEN is missing but not in CI if (!env.TOKEN && env.CI_ENV !== 'true') { console.warn('Warning: 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.TOKEN ? { Authorization: `token ${env.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 - Normalized environment variables for dependency injection * @returns Promise resolving to the latest tag from GitHub API */ async function getLatestTagFromGitHub(executor = exports.defaultExecutor, env) { // Use normalized environment variables const owner = env.REPOSITORY_OWNER; const repo = env.REPOSITORY_NAME; if (!owner || !repo) { const missingVars = []; if (!owner) missingVars.push('REPOSITORY_OWNER'); if (!repo) missingVars.push('REPOSITORY_NAME'); throw new Error(`Missing required GitHub environment variables: ${missingVars.join(', ')}`); } const url = `https://api.github.com/repos/${owner}/${repo}/tags`; try { const response = await executor.getGitHubData(url, env); if (!Array.isArray(response) || response.length === 0) { console.error('Unexpected response format from GitHub API:', typeof response === 'object' ? JSON.stringify(response || {}, null, 2) : response); throw new Error('Unexpected response format from GitHub API, URL: ' + url); } // 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) { console.error('Available tags:', response.map((tag) => tag.name).join(', ')); throw new Error('No tags starting with "v" found in repository, URL: ' + url); } // 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) { console.error('Available v-tags:', allVTags.map((tag) => tag.name).join(', ')); throw new Error('No tags matching the required format vX.Y found in repository, URL: ' + url); } return vTags[0].name; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`GitHub API error details: ${errorMessage}`); throw new Error(`Failed to get latest tag from GitHub, URL: ${url}, error: ${errorMessage}`); } } /** * Gets the commit count from GitHub API * * @param fromTag - The tag to count commits from * @param executor - Custom executor for dependency injection * @param env - Normalized environment variables for dependency injection * @returns Promise resolving to the commit count */ async function getCommitCountFromGitHub(fromTag, executor = exports.defaultExecutor, env) { // Use normalized environment variables const owner = env.REPOSITORY_OWNER; const repo = env.REPOSITORY_NAME; const sha = env.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, env); const aheadBy = response.ahead_by; if (typeof aheadBy !== 'number') { throw new Error(`Invalid response from GitHub API: ahead_by is not a number: ${aheadBy}\n\n${url}\n\n${response}`); } return aheadBy; } catch (error) { throw new Error(`Failed to get commit count 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 - Normalized environment variables for dependency injection * @param options.cwd - Current working directory * @returns Promise resolving to the latest tag */ async function getLatestTag({ executor, env, cwd, }) { const resolvedExecutor = executor || exports.defaultExecutor; // If running in CI, use the GitHub API if (env.CI_ENV === 'true') { try { return await getLatestTagFromGitHub(resolvedExecutor, env); } catch (error) { // Create a debug copy of env with TOKEN masked const debugEnv = Object.assign({}, env); debugEnv.TOKEN = debugEnv.TOKEN ? '<set and valid>' : '<unset>'; console.error('Environment variables used for GitHub API call:'); console.error(JSON.stringify(debugEnv, null, 2)); 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 = resolvedExecutor.execCommand(gitCommand, 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 git * * @param fromTag - The tag to count commits from * @param options - Options for getting the commit count * @param options.executor - Custom executor for dependency injection * @param options.env - Normalized environment variables for dependency injection * @param options.cwd - Current working directory * @returns Promise resolving to the commit count */ async function getCommitCount(fromTag, options) { const executor = options.executor || exports.defaultExecutor; const env = options.env; // No special case needed as we're failing when no tags are found if (env.CI_ENV === '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 * @param options.env - Normalized environment variables for dependency injection * @param options.cwd - Current working directory * @returns The current branch name */ function getCurrentBranch(options) { const executor = options.executor || exports.defaultExecutor; const env = options.env; // Use the normalized BRANCH_NAME which already handles PR vs non-PR branches if (env.BRANCH_NAME) { return env.BRANCH_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 * @param options.env - Normalized environment variables for dependency injection * @param options.cwd - Current working directory * @returns The short commit hash */ function getShortCommitHash(options) { const executor = options.executor || exports.defaultExecutor; const env = options.env; // Use the normalized SHA if (env.SHA) { return env.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 - Normalized environment variables for dependency injection * @param options.android - Android version options * @param options.ios - iOS 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; // 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, }; } /** * Write version information to a file * @param versionInfo - Version information object * @param filePath - Path to the file to write * @param options - Optional configuration * @param options.executor - Custom executor for testing * @param options.cwd - Current working directory */ function writeVersionToFile(versionInfo, filePath, options) { // If filePath is not absolute and cwd is provided, resolve it relative to cwd const resolvedFilePath = filePath.startsWith('/') || !options.cwd ? filePath : (0, path_1.join)(options.cwd, filePath); const fileContent = JSON.stringify(versionInfo, null, 2); // Create directory if it doesn't exist options.executor.mkdirSync((0, path_1.dirname)(resolvedFilePath), { recursive: true }); options.executor.writeFile(resolvedFilePath, fileContent); } /** * Generate a version and write it to a file * @param dir - Directory to use for command execution * @param outputFilePath - Optional output file path(s) (relative to dir if not absolute) * @param options - Options for version generation * @param options.executor - Optional executor for commands * @param options.env - Optional environment variables * @param options.android - Android version options * @param options.ios - iOS version options * @returns Promise resolving to the version information object */ async function generateAndWriteVersion(dir, outputFilePath, options = {}) { const executor = options.executor || exports.defaultExecutor; // Normalize environment variables at the entry point const env = (0, normalize_env_1.normalizeEnvironment)(Object.assign(Object.assign({}, process.env), (options.env || {}))); // Generate the version const versionInfo = await generatePackageVersion(dir, { executor, env, android: options.android, ios: options.ios, }); // If output file path(s) are provided, write the version to the file(s) if (outputFilePath) { if (Array.isArray(outputFilePath)) { // Write to multiple output files for (const filePath of outputFilePath) { writeVersionToFile(versionInfo, filePath, { executor, cwd: dir }); } } else { // Write to a single output file writeVersionToFile(versionInfo, outputFilePath, { executor, cwd: dir }); } } return versionInfo; } //# sourceMappingURL=index.js.map