Skip to content

Commit

Permalink
feat: add support for updating from a build version file
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandermendes committed Nov 27, 2024
1 parent ba004a7 commit 47a8038
Show file tree
Hide file tree
Showing 9 changed files with 434 additions and 65 deletions.
49 changes: 39 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,17 @@ The example configuration above will version and git commit your native files.

## Configuration

| Property | Description | Default |
|-------------------|--------------------------------------------------------------------|----------------------------|
| `androidPath` | Path to your "android/app/build.gradle" file. | `android/app/build.gradle` |
| `iosPath` | Path to your "ios/" folder. | `ios` |
| `skipBuildNumber` | Do not increment the build number for either platform. | `false` |
| `skipAndroid` | Skip Android versioning. | `false` |
| `skipIos` | Skip iOS versioning. | `false` |
| `iosPackageName` | Only update iOS projects that have the given name. | `null` |
| `noPrerelease` | Skip pre-release versions entirely for both platforms. | `false` |
| `versionStrategy` | Specifies the versioning strategies for each platform (see below). | `{"android": {"buildNumber": "increment", "preRelease": true}, "ios": {"buildNumber": "strict", "preRelease": true}}` |
| Property | Description | Default |
|-------------------|--------------------------------------------------------------------------------|----------------------------|
| `androidPath` | Path to your "android/app/build.gradle" file. | `android/app/build.gradle` |
| `iosPath` | Path to your "ios/" folder. | `ios` |
| `skipBuildNumber` | Do not increment the build number for either platform. | `false` |
| `skipAndroid` | Skip Android versioning. | `false` |
| `skipIos` | Skip iOS versioning. | `false` |
| `iosPackageName` | Only update iOS projects that have the given name. | `null` |
| `noPrerelease` | Skip pre-release versions entirely for both platforms. | `false` |
| `fromFile` | Use a JSON file (e.g. `.versionrc.json`) to read and write the version number. | `null` |
| `versionStrategy` | Specifies the versioning strategies for each platform (see below). | `{"android": {"buildNumber": "increment", "preRelease": true}, "ios": {"buildNumber": "strict", "preRelease": true}}` |

## Versioning strategies

Expand Down Expand Up @@ -243,3 +244,31 @@ If these variables are not present in the first place then nothing will be added
Whether or not you choose to commit these updates is up to you. If you are not
actually using these variables in your `Info.plist` files then they are probably
redundant anyway.

## Build version file

If you do not want to update the `Info.plist` and `build.gradle` files directly you
can read and write the build version to a JSON file instead, using the `fromFile` option.
This can be useful for supporting Expo projects, where this version file can then be loaded
into your [app config](https://docs.expo.dev/workflow/configuration/).

You can call this file whatever you like, for example:

```json
{
"plugins": [
["semantic-release-react-native", {
"fromFile": ".versionrc.json",
}],
]
}
```

The file will be output in the following format:

```json
{
"android": 5322,
"ios": "3837.15.99"
}
```
34 changes: 32 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ type ErrorCodes = 'ENRNANDROIDPATH'
| 'ENRNIOSPATH'
| 'ENRNNOTBOOLEAN'
| 'ENRNNOTSTRING'
| 'ENRNVERSIONSTRATEGY';
| 'ENRNVERSIONSTRATEGY'
| 'ENRNFROMFILENOTJSON';

type ErrorDefinition = (key: keyof PluginConfig) => {
message: string;
Expand Down Expand Up @@ -51,10 +52,39 @@ const ERROR_DEFINITIONS: Record<ErrorCodes, ErrorDefinition> = {
message: `Invalid ${key}`,
details: `The ${key} must comply with the schema (see docs).`,
}),
ENRNFROMFILENOTJSON: (key: keyof PluginConfig) => ({
message: `Invalid ${key}`,
details: `The ${key} must point to a JSON file.`,
}),
};

export const getError = (key: keyof PluginConfig, code: ErrorCodes) => {
export const getSemanticReleaseError = (key: keyof PluginConfig, code: ErrorCodes) => {
const { message, details } = ERROR_DEFINITIONS[code](key);

return new SemanticReleaseError(message, code, details);
};

const isError = (error: unknown): error is Error => (
typeof error === 'object'
&& error !== null
&& 'message' in error
&& typeof (error as Record<string, unknown>).message === 'string'
);

export const toError = (maybeError: unknown): Error => {
if (isError(maybeError)) {
return maybeError;
}

if (typeof maybeError === 'string') {
return new Error(maybeError);
}

try {
return new Error(JSON.stringify(maybeError));
} catch {
// Fallback in case there's an error stringifying the maybeError,
// like with circular references, for example.
return new Error(String(maybeError));
}
};
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type PluginConfig = {
skipAndroid?: boolean;
skipIos?: boolean;
noPrerelease?: boolean;
fromFile?: string;
versionStrategy?: {
android?: {
buildNumber?: AndroidVersionStrategies;
Expand All @@ -25,3 +26,8 @@ export type PluginConfig = {
};

export type FullPluginConfig = Required<PluginConfig>;

export type VersionFile = {
android?: string;
ios?: string;
};
44 changes: 33 additions & 11 deletions src/verify.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import fs from 'fs';
import path from 'path';
import appRoot from 'app-root-path';
import { PluginConfig } from './types';
import { toAbsolutePath } from './paths';
import { getError } from './errors';
import { getSemanticReleaseError } from './errors';
import { iosVesionStrategies } from './strategies';

const verifyAndroidPath = (androidPath: string) => {
const absAndroidPath = toAbsolutePath(androidPath);

if (!absAndroidPath.endsWith('build.gradle')) {
return getError('androidPath', 'ENRNANDROIDPATH');
return getSemanticReleaseError('androidPath', 'ENRNANDROIDPATH');
}

if (!fs.existsSync(absAndroidPath)) {
return getError('androidPath', 'ENRNANDROIDPATH');
return getSemanticReleaseError('androidPath', 'ENRNANDROIDPATH');
}

return null;
Expand All @@ -22,11 +24,27 @@ const verifyIosPath = (iosPath: string) => {
const absIosPath = toAbsolutePath(iosPath);

if (!fs.existsSync(absIosPath)) {
return getError('iosPath', 'ENRNIOSPATH');
return getSemanticReleaseError('iosPath', 'ENRNIOSPATH');
}

if (!fs.lstatSync(absIosPath).isDirectory()) {
return getError('iosPath', 'ENRNIOSPATH');
return getSemanticReleaseError('iosPath', 'ENRNIOSPATH');
}

return null;
};

const verifyJSONFile = (fileName: string) => {
const filePath = path.join(appRoot.path, fileName);

if (!fs.existsSync(filePath)) {
return null;
}

try {
JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (error) {
return getSemanticReleaseError('fromFile', 'ENRNFROMFILENOTJSON');
}

return null;
Expand All @@ -43,6 +61,10 @@ export const verifyConditons = (pluginConfig: PluginConfig) => {
errors.push(verifyIosPath(pluginConfig.iosPath));
}

if (pluginConfig.fromFile) {
errors.push(verifyJSONFile(pluginConfig.fromFile));
}

const booleanValues: (keyof PluginConfig)[] = [
'skipBuildNumber',
'skipAndroid',
Expand All @@ -52,33 +74,33 @@ export const verifyConditons = (pluginConfig: PluginConfig) => {

booleanValues.forEach((key) => {
if (key in pluginConfig && typeof pluginConfig[key] !== 'boolean') {
errors.push(getError(key, 'ENRNNOTBOOLEAN'));
errors.push(getSemanticReleaseError(key, 'ENRNNOTBOOLEAN'));
}
});

if (
'iosPackageName' in pluginConfig
&& !(typeof pluginConfig.iosPackageName === 'string' || pluginConfig.iosPackageName instanceof String)
) {
errors.push(getError('iosPackageName', 'ENRNNOTSTRING'));
errors.push(getSemanticReleaseError('iosPackageName', 'ENRNNOTSTRING'));
}

const { ios, android } = pluginConfig.versionStrategy ?? {};

if (ios?.buildNumber && !iosVesionStrategies.includes(ios.buildNumber)) {
errors.push(getError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
errors.push(getSemanticReleaseError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
}

if (android?.buildNumber && !iosVesionStrategies.includes(android.buildNumber)) {
errors.push(getError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
errors.push(getSemanticReleaseError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
}

if (ios?.preRelease != null && typeof ios.preRelease !== 'boolean') {
errors.push(getError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
errors.push(getSemanticReleaseError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
}

if (android?.preRelease != null && typeof android.preRelease !== 'boolean') {
errors.push(getError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
errors.push(getSemanticReleaseError('versionStrategy', 'ENRNVERSIONSTRATEGY'));
}

return errors.filter((x) => x);
Expand Down
42 changes: 41 additions & 1 deletion src/version/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import path from 'path';
import type { Context } from 'semantic-release';
import type { FullPluginConfig } from '../types';
import { toAbsolutePath } from '../paths';
import { getSemanticBuildNumber, isPreRelease } from './utils';
import {
getSemanticBuildNumber,
isPreRelease,
loadBuildVersionFile,
writeBuildVersionFile,
} from './utils';

/**
* Get the path to the Android bundle.gradle file.
Expand Down Expand Up @@ -83,6 +88,34 @@ const getNextAndroidVersionCode = (
return String(parseInt(currentVersionCode, 10) + 1);
};

/**
* Update a version file, rather than the build.gradle.
*/
const versionFromFile = (
{ versionStrategy, fromFile, skipBuildNumber }: FullPluginConfig,
{ logger }: Context,
version: string,
) => {
if (skipBuildNumber) {
logger.info('Skipping update of Android build number');

return;
}

const versionFile = loadBuildVersionFile(fromFile);
const nextBuildVersion = getNextAndroidVersionCode(
versionStrategy.android,
logger,
version,
versionFile.android ?? '0',
);

writeBuildVersionFile(fromFile, {
...versionFile,
android: nextBuildVersion,
});
};

/**
* Update Android files with the new version.
*
Expand All @@ -108,6 +141,13 @@ export const versionAndroid = (
return;
}

if (pluginConfig.fromFile) {
logger.info('Versioning Android from file');
versionFromFile(pluginConfig, context, version);

return;
}

logger.info('Versioning Android');

const androidPath = getAndroidPath(pluginConfig.androidPath);
Expand Down
43 changes: 42 additions & 1 deletion src/version/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import htmlMinifier from 'html-minifier';
import type { Context } from 'semantic-release';
import type { FullPluginConfig } from '../types';
import { toAbsolutePath } from '../paths';
import { getSemanticBuildNumber, isPreRelease, stripPrereleaseVersion } from './utils';
import {
getSemanticBuildNumber,
isPreRelease,
loadBuildVersionFile,
stripPrereleaseVersion,
writeBuildVersionFile,
} from './utils';

/**
* Get the path to the iOS Xcode project file.
Expand Down Expand Up @@ -375,6 +381,34 @@ const incrementPlistVersions = (
});
};

/**
* Update a version file, rather than the Info.plist and Xcode project files.
*/
const versionFromFile = (
{ versionStrategy, fromFile, skipBuildNumber }: FullPluginConfig,
{ logger }: Context,
version: string,
) => {
if (skipBuildNumber) {
logger.info('Skipping update of iOS Android build number');

return;
}

const versionFile = loadBuildVersionFile(fromFile);
const nextBuildVersion = getIosBundleVersion(
versionStrategy.ios,
logger,
versionFile.ios ?? '0',
version,
);

writeBuildVersionFile(fromFile, {
...versionFile,
ios: nextBuildVersion,
});
};

/**
* Version iOS files.
*/
Expand Down Expand Up @@ -407,6 +441,13 @@ export const versionIos = (
return;
}

if (pluginConfig.fromFile) {
logger.info('Versioning iOS from file');
versionFromFile(pluginConfig, context, version);

return;
}

logger.info('Versioning iOS');

const iosPath = getIosPath(pluginConfig.iosPath);
Expand Down
Loading

0 comments on commit 47a8038

Please sign in to comment.