Skip to content

Commit af32d6f

Browse files
authored
feat: add remote-cache command (#263)
* refactor: remote cache provider * update * remove console * fixup * rename * cleanup * fixup * simplify finding binaries * changeset * feat: add remote-cache command and internal plugin * simplify finding binaries * wip remove/upload * update * list-all, download, delete * set raw output * fix: skip hidden files when finding binary path * fixup finding binary * convert commands to internal plugins * feat: add missing methods to remote cache provider * add test * unify interface * mark delete and upload as optional * changeset * simplify return to array only * remove id * simplify upload return * refactor getLocalBinaryPath * get rid of local artifact path as it's handled by RNEF; pass targetURL * make download return Reponse and move logic to RNEF * rework provider plugin to be configurable * adjust to new provider * update upload an delete * add validation and logging * add -t alias for --traits * adjust name for consistency * cleanup * changeset * take traits into account
1 parent 184cc74 commit af32d6f

File tree

7 files changed

+251
-8
lines changed

7 files changed

+251
-8
lines changed

.changeset/nine-tables-nail.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rnef/tools': patch
3+
'@rnef/cli': patch
4+
---
5+
6+
feat: add remote-cache command

packages/cli/src/lib/cli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Command } from 'commander';
77
import { checkDeprecatedOptions } from './checkDeprecatedOptions.js';
88
import { fingerprintPlugin } from './plugins/fingerprint.js';
99
import { logConfigPlugin } from './plugins/logConfig.js';
10+
import { remoteCachePlugin } from './plugins/remoteCache.js';
1011

1112
const require = createRequire(import.meta.url);
1213
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -31,7 +32,11 @@ export const cli = async ({ cwd, argv }: CliOptions) => {
3132
.option('--verbose', 'enable verbose logging')
3233
.version(version);
3334

34-
const internalPlugins = [logConfigPlugin, fingerprintPlugin];
35+
const internalPlugins = [
36+
remoteCachePlugin,
37+
logConfigPlugin,
38+
fingerprintPlugin,
39+
];
3540
// Register commands from the config
3641
const config = await getConfig(cwd, internalPlugins);
3742

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import type { PluginApi, PluginOutput } from '@rnef/config';
2+
import type {
3+
RemoteBuildCache,
4+
SupportedRemoteCacheProviders,
5+
} from '@rnef/tools';
6+
import {
7+
createRemoteBuildCache,
8+
formatArtifactName,
9+
getLocalArtifactPath,
10+
getLocalBinaryPath,
11+
handleDownloadResponse,
12+
logger,
13+
RnefError,
14+
spinner,
15+
} from '@rnef/tools';
16+
17+
type Flags = {
18+
platform?: 'ios' | 'android';
19+
traits?: string[];
20+
name?: string;
21+
json?: boolean;
22+
};
23+
24+
async function remoteCache({
25+
action,
26+
args,
27+
remoteCacheProvider,
28+
projectRoot,
29+
fingerprintOptions,
30+
}: {
31+
action: string;
32+
args: Flags;
33+
remoteCacheProvider:
34+
| SupportedRemoteCacheProviders
35+
| null
36+
| { (): RemoteBuildCache };
37+
projectRoot: string;
38+
fingerprintOptions: { extraSources: string[]; ignorePaths: string[] };
39+
}) {
40+
const isJsonOutput = args.json;
41+
const remoteBuildCache = await createRemoteBuildCache(remoteCacheProvider);
42+
if (!remoteBuildCache) {
43+
return null;
44+
}
45+
46+
validateArgs(args, action);
47+
48+
const artifactName =
49+
args.name ??
50+
(await formatArtifactName({
51+
platform: args.platform,
52+
traits: args.traits,
53+
root: projectRoot,
54+
fingerprintOptions,
55+
}));
56+
57+
switch (action) {
58+
case 'list': {
59+
const artifacts = await remoteBuildCache.list({ artifactName });
60+
if (artifacts) {
61+
if (isJsonOutput) {
62+
console.log(JSON.stringify(artifacts[0], null, 2));
63+
} else {
64+
logger.log(`- name: ${artifacts[0].name}
65+
- url: ${artifacts[0].url}`);
66+
}
67+
} else {
68+
throw new RnefError(`No artifacts found for "${artifactName}".`);
69+
}
70+
break;
71+
}
72+
case 'list-all': {
73+
const artifactName = undefined;
74+
const artifacts = await remoteBuildCache.list({ artifactName });
75+
const platform = args.platform;
76+
const traits = args.traits;
77+
const output =
78+
platform && traits
79+
? artifacts.filter((artifact) =>
80+
artifact.name.startsWith(`rnef-${platform}-${traits.join('-')}`)
81+
)
82+
: artifacts;
83+
if (isJsonOutput) {
84+
console.log(JSON.stringify(output, null, 2));
85+
} else {
86+
output.forEach((artifact) => {
87+
logger.log(`- name: ${artifact.name}
88+
- url: ${artifact.url}`);
89+
});
90+
}
91+
break;
92+
}
93+
case 'download': {
94+
const localArtifactPath = getLocalArtifactPath(artifactName);
95+
const response = await remoteBuildCache.download({ artifactName });
96+
const loader = spinner();
97+
await handleDownloadResponse(
98+
response,
99+
localArtifactPath,
100+
artifactName,
101+
loader
102+
);
103+
const binaryPath = getLocalBinaryPath(localArtifactPath);
104+
if (!binaryPath) {
105+
throw new RnefError(`No binary found for "${artifactName}".`);
106+
}
107+
if (isJsonOutput) {
108+
console.log(
109+
JSON.stringify({ name: artifactName, path: binaryPath }, null, 2)
110+
);
111+
} else {
112+
logger.log(`- name: ${artifactName}
113+
- path: ${binaryPath}`);
114+
}
115+
break;
116+
}
117+
case 'upload': {
118+
const uploadedArtifact = await remoteBuildCache.upload({ artifactName });
119+
if (isJsonOutput) {
120+
console.log(JSON.stringify(uploadedArtifact, null, 2));
121+
} else {
122+
logger.log(`- name: ${uploadedArtifact.name}
123+
- url: ${uploadedArtifact.url}`);
124+
}
125+
break;
126+
}
127+
case 'delete': {
128+
const deletedArtifacts = await remoteBuildCache.delete({ artifactName });
129+
if (isJsonOutput) {
130+
console.log(JSON.stringify(deletedArtifacts, null, 2));
131+
} else {
132+
logger.log(
133+
deletedArtifacts
134+
.map(
135+
(artifact) => `- name: ${artifact.name}
136+
- url: ${artifact.url}`
137+
)
138+
.join('\n')
139+
);
140+
}
141+
break;
142+
}
143+
}
144+
145+
return null;
146+
}
147+
148+
function validateArgs(args: Flags, action: string) {
149+
if (!action) {
150+
// @todo make Commander handle this
151+
throw new RnefError(
152+
'Action is required. Available actions: list, list-all, download, upload, delete'
153+
);
154+
}
155+
if (args.name && (args.platform || args.traits)) {
156+
throw new RnefError(
157+
'Cannot use "--name" together with "--platform" or "--traits". Use either name or platform with traits'
158+
);
159+
}
160+
if (!args.name) {
161+
if ((args.platform && !args.traits) || (!args.platform && args.traits)) {
162+
throw new RnefError(
163+
'Either "--platform" and "--traits" must be provided together'
164+
);
165+
}
166+
if (!args.platform || !args.traits) {
167+
throw new RnefError(
168+
'Either "--name" or "--platform" and "--traits" must be provided'
169+
);
170+
}
171+
}
172+
}
173+
174+
export const remoteCachePlugin =
175+
() =>
176+
(api: PluginApi): PluginOutput => {
177+
api.registerCommand({
178+
name: 'remote-cache',
179+
description: 'Manage remote cache',
180+
action: async (action: string, args: Flags) => {
181+
await remoteCache({
182+
action,
183+
args,
184+
remoteCacheProvider: api.getRemoteCacheProvider() || null,
185+
projectRoot: api.getProjectRoot(),
186+
fingerprintOptions: api.getFingerprintOptions(),
187+
});
188+
},
189+
args: [
190+
{
191+
name: '[action]',
192+
description: 'Select action, e.g. query, download, upload, delete',
193+
},
194+
],
195+
options: [
196+
{
197+
name: '--json',
198+
description: 'Output in JSON format',
199+
},
200+
{
201+
name: '--name <string>',
202+
description: 'Full artifact name',
203+
},
204+
{
205+
name: '-p, --platform <string>',
206+
description: 'Select platform, e.g. ios or android',
207+
},
208+
{
209+
name: '-t, --traits <list>',
210+
description: `Comma-separated traits that construct final artifact name. Traits for Android are: variant; for iOS: destination and configuration.
211+
Example iOS: --traits simulator,Release
212+
Example Android: --traits debug`,
213+
parse: (val: string) => val.split(','),
214+
},
215+
],
216+
});
217+
218+
return {
219+
name: 'internal_remote-cache',
220+
description: 'Manage remote cache',
221+
};
222+
};

packages/tools/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ export { isInteractive } from './lib/isInteractive.js';
1919
export { spawn, SubprocessError } from './lib/spawn.js';
2020
export { color } from './lib/color.js';
2121
export { runHermes } from './lib/hermes.js';
22-
export { fetchCachedBuild } from './lib/build-cache/fetchCachedBuild.js';
22+
export {
23+
fetchCachedBuild,
24+
handleDownloadResponse,
25+
} from './lib/build-cache/fetchCachedBuild.js';

packages/tools/src/lib/build-cache/common.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type SupportedRemoteCacheProviders = 'github-actions';
1010
export type RemoteArtifact = {
1111
name: string;
1212
url: string;
13+
id?: string; // optional, for example for GitHub Actions
1314
};
1415

1516
export type LocalArtifact = {
@@ -75,11 +76,14 @@ export async function formatArtifactName({
7576
root,
7677
fingerprintOptions,
7778
}: {
78-
platform: 'ios' | 'android';
79-
traits: string[];
79+
platform?: 'ios' | 'android';
80+
traits?: string[];
8081
root: string;
8182
fingerprintOptions: { extraSources: string[]; ignorePaths: string[] };
8283
}): Promise<string> {
84+
if (!platform || !traits) {
85+
return '';
86+
}
8387
const { hash } = await nativeFingerprint(root, {
8488
platform,
8589
...fingerprintOptions,

packages/tools/src/lib/build-cache/fetchCachedBuild.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Proceeding with local build.`);
9292
};
9393
}
9494

95-
async function handleDownloadResponse(
95+
export async function handleDownloadResponse(
9696
response: Response,
9797
localArtifactPath: string,
9898
name: string,

packages/tools/src/lib/build-cache/github/GitHubBuildCache.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import type { RemoteArtifact, RemoteBuildCache } from '../common.js';
32
import {
43
deleteGitHubArtifacts,
@@ -44,6 +43,7 @@ export class GitHubBuildCache implements RemoteBuildCache {
4443
return artifacts.map((artifact) => ({
4544
name: artifact.name,
4645
url: artifact.downloadUrl,
46+
id: String(artifact.id),
4747
}));
4848
}
4949

@@ -83,10 +83,13 @@ export class GitHubBuildCache implements RemoteBuildCache {
8383
}
8484

8585
async upload(): Promise<RemoteArtifact> {
86-
throw new Error('Uploading artifacts to GitHub is not supported through GitHub API. See: https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28');
86+
throw new Error(
87+
'Uploading artifacts to GitHub is not supported through GitHub API. See: https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28'
88+
);
8789
}
8890
}
8991

9092
export const pluginGitHubBuildCache =
91-
(options?: { owner: string; repository: string; token: string }) => (): RemoteBuildCache =>
93+
(options?: { owner: string; repository: string; token: string }) =>
94+
(): RemoteBuildCache =>
9295
new GitHubBuildCache(options);

0 commit comments

Comments
 (0)