@wellsite/version-generator
Version:
Generates Versions based on git information
269 lines • 11.5 kB
JavaScript
;
/**
* iOS build number generation utilities
* Handles interaction with App Store Connect API to get and increment build numbers
*/
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.DefaultIosExecutor = void 0;
exports.getIosBuildNumberInfo = getIosBuildNumberInfo;
exports.encodeCommitHash = encodeCommitHash;
exports.composeIosBuildVersion = composeIosBuildVersion;
exports.processBuilds = processBuilds;
exports.getAppStoreVersionInfo = getAppStoreVersionInfo;
const jwt = __importStar(require("jsonwebtoken"));
const https = __importStar(require("https"));
const base64_1 = require("./base64");
/**
* Gets detailed iOS build number information
*
* @param options - Options for getting the iOS build number
* @param versionInfoProvider - Function to get version info from App Store Connect
* @param jwtGenerator - JWT token generator
* @param apiClient - App Store Connect API client
* @param buildProcessor - Build data processor
* @returns Promise resolving to the iOS build number info
*/
/**
* Default implementation of IosExecutor
*/
class DefaultIosExecutor {
/**
* Generates a JWT token for App Store Connect API authentication
*
* @param keyId - The App Store Connect API Key ID
* @param issuerId - The App Store Connect API Issuer ID
* @param privateKey - The App Store Connect API Private Key
* @returns The JWT token
*/
generateToken(keyId, issuerId, privateKey) {
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: issuerId,
exp: now + 20 * 60, // Token expires in 20 minutes
aud: 'appstoreconnect-v1',
};
const signOptions = {
algorithm: 'ES256',
header: {
alg: 'ES256',
kid: keyId,
typ: 'JWT',
},
};
return jwt.sign(payload, privateKey, signOptions);
}
/**
* Queries the App Store Connect API for builds
*
* @param bundleId - The bundle ID of the iOS app
* @param appReleaseVersion - The app release version (CFBundleShortVersionString)
* @param token - The JWT token for authentication
* @returns Promise resolving to the builds data
*/
async queryApi(bundleId, appReleaseVersion, token) {
// Encode the filter parameters for the URL
const encodedBundleId = encodeURIComponent(bundleId);
const encodedVersion = encodeURIComponent(appReleaseVersion);
// First, we need to find the app ID using the bundle ID
const appUrl = `https://api.appstoreconnect.apple.com/v1/apps?filter[bundleId]=${encodedBundleId}`;
// Get the app info first
const appInfo = await this.makeApiRequest(appUrl, token);
// Check if we found the app
if (!appInfo.data || appInfo.data.length === 0) {
throw new Error(`No app found with bundle ID: ${bundleId}`);
}
// Get the app ID from the response
const appId = appInfo.data[0].id;
// Now construct the URL for builds with the app ID and version
const url = `https://api.appstoreconnect.apple.com/v1/builds?filter[app]=${appId}&filter[version]=${encodedVersion}`;
return this.makeApiRequest(url, token);
}
/**
* Makes a request to the App Store Connect API
*
* @param url - The API URL to request
* @param token - The JWT token for authentication
* @returns Promise resolving to the API response data
*/
makeApiRequest(url, token) {
return new Promise((resolve, reject) => {
const req = https.get(url, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'User-Agent': 'Version Generator',
},
}, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
const jsonData = JSON.parse(data);
resolve(jsonData);
}
catch (error) {
reject(new Error(`Failed to parse App Store Connect API response: ${error instanceof Error ? error.message : String(error)}`));
}
}
else {
reject(new Error(`App Store Connect API request failed with status ${res.statusCode}: ${data}`));
}
});
});
req.on('error', (error) => {
reject(new Error(`App Store Connect API request failed: ${error.message}`));
});
req.end();
});
}
}
exports.DefaultIosExecutor = DefaultIosExecutor;
async function getIosBuildNumberInfo(options, executor = new DefaultIosExecutor()) {
try {
if (!options.enabled) {
return { buildNumber: 0, buildVersion: '0' };
}
if (!options.apiKeyId ||
!options.apiIssuerId ||
!options.apiPrivateKey ||
!options.bundleId ||
!options.appReleaseVersion) {
throw new Error('iOS build number generation requires apiKeyId, apiIssuerId, apiPrivateKey, bundleId, and appReleaseVersion');
}
// Get version info from App Store Connect API
const versionInfo = await getAppStoreVersionInfo(options.bundleId, options.appReleaseVersion, options.apiKeyId, options.apiIssuerId, options.apiPrivateKey, executor);
// If no version info found, start with build number 1
if (!versionInfo) {
return composeIosBuildVersion(1, options.commitHash);
}
// Increment the highest build number found
return composeIosBuildVersion(versionInfo.highestBuildNumber + 1, options.commitHash);
}
catch (error) {
throw new Error(`Failed to get iOS build number: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Encodes a commit hash into an integer for use in iOS build numbers
*
* @param commitHash - The commit hash to encode
* @returns The encoded commit hash as an integer
*/
function encodeCommitHash(commitHash) {
// Take the first 8 characters of the commit hash (this is commonly used as a short hash)
const shortHash = commitHash.substring(0, 8);
// Convert the hex string to a decimal number
// This could be very large, so we'll take modulo to ensure it fits in a 32-bit integer
// (iOS build numbers are typically expected to be 32-bit integers)
return parseInt(shortHash, 16) % 2147483647; // Max 32-bit signed integer
}
/**
* Gets the iOS build number with dependency injection for testing
*
* @param options - Options for getting the iOS build number
* @param versionInfoProvider - Function to get version info from App Store Connect
* @returns Promise resolving to the next iOS build number to use
*/
/**
* Composes an iOS build version object from a build number and optional commit hash
*
* @param buildNumber - The build number
* @param commitHash - Optional commit hash to encode in the build version
* @returns The composed iOS build number info
*/
function composeIosBuildVersion(buildNumber, commitHash) {
// Create the build number info object
const buildNumberInfo = {
buildNumber,
buildVersion: buildNumber.toString(),
};
// If commit hash is provided, encode it and add to the build number
if (commitHash) {
buildNumberInfo.encodedCommitHash = encodeCommitHash(commitHash);
buildNumberInfo.buildVersion = `${buildNumber}.${buildNumberInfo.encodedCommitHash}`;
}
return buildNumberInfo;
}
/**
* Processes the builds data to find the highest build number
*
* @param builds - The builds data from the App Store Connect API
* @param appReleaseVersion - The app release version (CFBundleShortVersionString)
* @returns The version info with the highest build number
*/
function processBuilds(builds, appReleaseVersion) {
// Check if there are any builds
if (!builds || !builds.data || !Array.isArray(builds.data) || builds.data.length === 0) {
return undefined;
}
let highestBuildNumber = 0;
// Process each build to find the highest build number
for (const build of builds.data) {
if (build.attributes && build.attributes.version === appReleaseVersion) {
const buildNumber = parseInt(build.attributes.buildNumber, 10);
if (!isNaN(buildNumber) && buildNumber > highestBuildNumber) {
highestBuildNumber = buildNumber;
}
}
}
// If no valid build number was found, return undefined
if (highestBuildNumber === 0) {
return undefined;
}
return { highestBuildNumber };
}
async function getAppStoreVersionInfo(bundleId, appReleaseVersion, apiKeyId, apiIssuerId, apiPrivateKey, executor = new DefaultIosExecutor()) {
try {
// Decode the private key if it's base64-encoded
// Private key is not expected to be JSON, so we pass false for expectJson
const privateKey = (0, base64_1.isBase64)(apiPrivateKey, false)
? Buffer.from(apiPrivateKey, 'base64').toString('utf8')
: apiPrivateKey;
// Generate JWT token for authentication
const token = executor.generateToken(apiKeyId, apiIssuerId, privateKey);
// Query App Store Connect API for builds
const builds = await executor.queryApi(bundleId, appReleaseVersion, token);
// Process the builds to find the highest build number
return processBuilds(builds, appReleaseVersion);
}
catch (error) {
throw new Error(`Failed to get App Store version info: ${error instanceof Error ? error.message : String(error)}`);
}
}
//# sourceMappingURL=ios-version.js.map