Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ jobs:
- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
- name: Test native release checks
run: node --test scripts/check-native-releases.test.mjs
- run: corepack enable
- run: yarn install --immutable
- run: yarn typecheck
- run: yarn test
- run: yarn prepare
- name: Test Expo plugin resolution
run: node --test scripts/expo-plugin.test.mjs
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ jobs:
echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $version"
exit 1
fi
- name: Require published native SDKs
run: node scripts/check-native-releases.mjs
- run: corepack enable
- run: yarn install --immutable
- run: yarn typecheck
- run: yarn test
- run: yarn prepare
- name: Test Expo plugin resolution
run: node --test scripts/expo-plugin.test.mjs
# Trusted publishing is used when configured on npmjs.com; NPM_TOKEN is
# only needed for the first publish, before the package exists.
- run: npm publish --provenance
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@exostack/carillon-react-native",
"version": "0.4.1",
"version": "0.4.2",
"description": "React Native bridge over the Carillon native SDKs.",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",
Expand All @@ -10,7 +10,9 @@
"types": "./lib/typescript/src/index.d.ts",
"default": "./lib/module/index.js"
},
"./package.json": "./package.json"
"./package.json": "./package.json",
"./app.plugin.js": "./app.plugin.js",
"./app.plugin": "./app.plugin.js"
},
"files": [
"src",
Expand Down
84 changes: 84 additions & 0 deletions scripts/check-native-releases.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { promisify } from "node:util";
import { pathToFileURL } from "node:url";

export function nativeVersions(gradle, podspec) {
const kotlin = gradle.match(/\bsdkVersion:\s*"(\d+\.\d+\.\d+)"/)?.[1];
const swift = podspec.match(/\bminimumVersion:\s*"(\d+\.\d+\.\d+)"/)?.[1];
if (!kotlin || !swift)
throw new Error(
"Cannot read native SDK versions from the Gradle build and podspec.",
);
return { kotlin, swift };
}

export async function checkNativeReleases(
{ kotlin, swift },
{
request = fetch,
listTags = (url, ref) =>
promisify(execFile)(
"git",
["ls-remote", "--exit-code", "--tags", url, ref],
{ timeout: 30_000 },
),
} = {},
) {
const ref = `refs/tags/${swift}`;
let tags;
try {
tags = await listTags(
"https://github.com/exostack/carillon-swift.git",
ref,
);
} catch (cause) {
throw new Error(
`Swift SDK ${swift} is not available. Publish its tag before releasing React Native.`,
{ cause },
);
}
if (!tags.stdout.split("\n").some((line) => line.split("\t")[1] === ref)) {
throw new Error(
`Swift SDK ${swift} tag is missing. Publish it before releasing React Native.`,
);
}
// A successful publishing workflow is not enough: consumers need both artifacts on Central.
for (const extension of ["pom", "aar"]) {
const url = `https://repo.maven.apache.org/maven2/com/exostack/carillon/${kotlin}/carillon-${kotlin}.${extension}`;
const response = await request(url, {
method: "HEAD",
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
throw new Error(
`Kotlin SDK ${kotlin} ${extension} is not available on Maven Central (HTTP ${response.status}). Publish it and wait for availability before releasing React Native.`,
);
}
}
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
try {
const versions = nativeVersions(
await readFile(
new URL("../android/build.gradle", import.meta.url),
"utf8",
),
await readFile(
new URL("../CarillonReactNative.podspec", import.meta.url),
"utf8",
),
);
await checkNativeReleases(versions);
console.log(
`Native dependencies are published: Swift ${versions.swift}, Kotlin ${versions.kotlin}.`,
);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
71 changes: 71 additions & 0 deletions scripts/check-native-releases.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
checkNativeReleases,
nativeVersions,
} from "./check-native-releases.mjs";

const versions = { kotlin: "0.4.0", swift: "0.4.0" };
const listTags = async () => ({ stdout: "abcdef\trefs/tags/0.4.0\n" });

test("reads each version from its consumer declaration", () => {
assert.deepEqual(
nativeVersions('sdkVersion: "0.4.0"', 'minimumVersion: "0.5.0"'),
{ kotlin: "0.4.0", swift: "0.5.0" },
);
assert.throws(() => nativeVersions("", ""), /Cannot read/);
});

test("requires both Maven artifacts and the Swift tag", async () => {
const urls = [];
await checkNativeReleases(versions, {
listTags,
request: async (url) => {
urls.push(url);
return { ok: true };
},
});
assert.deepEqual(
urls.map((url) => url.split(".").at(-1)),
["pom", "aar"],
);
});

for (const missing of ["pom", "aar"]) {
test(`blocks publication when the Maven ${missing} is missing`, async () => {
await assert.rejects(
checkNativeReleases(versions, {
listTags,
request: async (url) => ({ ok: !url.endsWith(missing), status: 404 }),
}),
/not available on Maven Central/,
);
});
}

test("blocks publication when the Swift tag is missing", async () => {
await assert.rejects(
checkNativeReleases(versions, { listTags: async () => ({ stdout: "" }) }),
/tag is missing/,
);
});

test("fails closed on network errors", async () => {
await assert.rejects(
checkNativeReleases(versions, {
listTags: async () => {
throw new Error("network");
},
}),
/Swift SDK/,
);
await assert.rejects(
checkNativeReleases(versions, {
listTags,
request: async () => {
throw new Error("network");
},
}),
/network/,
);
});
31 changes: 31 additions & 0 deletions scripts/expo-plugin.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { test } from "node:test";

const require = createRequire(import.meta.url);
const {
resolvePluginForModule,
} = require("@expo/config-plugins/build/utils/plugin-resolver.js");
const root = fileURLToPath(new URL("../", import.meta.url));
const entry = fileURLToPath(new URL("../app.plugin.js", import.meta.url));

test("exports the Expo plugin entry point", () => {
assert.equal(
require.resolve("@exostack/carillon-react-native/app.plugin.js"),
entry,
);
assert.equal(
typeof require("@exostack/carillon-react-native/app.plugin.js"),
"function",
);
});

test("Expo resolves the plugin by package name", () => {
const resolved = resolvePluginForModule(
root,
"@exostack/carillon-react-native",
);
assert.equal(resolved.filePath, entry);
assert.equal(resolved.isPluginFile, true);
});
Loading