@wellsite/version-generator
Version:
Generates Versions based on git information
443 lines • 19.5 kB
JavaScript
;
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