@@ -3,12 +3,14 @@ import { color, colorLink } from '../color.js';
33import type { RockError } from '../error.js' ;
44import { getAllIgnorePaths } from '../fingerprint/ignorePaths.js' ;
55import { type FingerprintSources } from '../fingerprint/index.js' ;
6+ import { isInteractive } from '../isInteractive.js' ;
67import logger from '../logger.js' ;
78import { getProjectRoot } from '../project.js' ;
9+ import { promptConfirm } from '../prompts.js' ;
810import { spawn } from '../spawn.js' ;
9- import type { RemoteBuildCache } from './common.js' ;
11+ import { formatArtifactName , type RemoteBuildCache } from './common.js' ;
1012import { fetchCachedBuild } from './fetchCachedBuild.js' ;
11- import { getLocalBuildCacheBinaryPath } from './localBuildCache.js' ;
13+ import { getLocalBuildCacheBinaryPath , hasUsedRemoteCacheBefore } from './localBuildCache.js' ;
1214
1315export async function getBinaryPath ( {
1416 artifactName,
@@ -37,50 +39,45 @@ export async function getBinaryPath({
3739
3840 // 3. If not, check if the remote cache is requested
3941 if ( ! binaryPath && ! localFlag ) {
40- try {
41- const cachedBuild = await fetchCachedBuild ( {
42- artifactName,
43- remoteCacheProvider,
44- } ) ;
45- if ( cachedBuild ) {
46- binaryPath = cachedBuild . binaryPath ;
47- }
48- } catch ( error ) {
49- const message = ( error as RockError ) . message ;
50- const cause = ( error as RockError ) . cause ;
51- logger . warn (
52- `Remote Cache: Failed to fetch cached build for ${ color . bold (
53- artifactName ,
54- ) } .
55- Cause: ${ message } ${ cause ? `\n${ cause . toString ( ) } ` : '' }
56- Read more: ${ colorLink (
57- 'https://rockjs.dev/docs/configuration#remote-cache-configuration' ,
58- ) } `,
59- ) ;
60- await warnIgnoredFiles ( fingerprintOptions , platformName , sourceDir ) ;
61- logger . debug ( 'Remote cache failure error:' , error ) ;
62- logger . info ( 'Continuing with local build' ) ;
63- }
42+ binaryPath = await tryFetchCachedBuild ( {
43+ artifactName,
44+ remoteCacheProvider,
45+ fingerprintOptions,
46+ platformName,
47+ sourceDir,
48+ } ) ;
6449 }
6550
6651 return binaryPath ;
6752}
6853
69- async function warnIgnoredFiles (
70- fingerprintOptions : FingerprintSources ,
71- platformName : string ,
72- sourceDir : string ,
73- ) {
74- // @todo unify git helpers from create-app
54+ /**
55+ * Checks if the current directory is a git repository
56+ */
57+ async function isGitRepository ( sourceDir : string ) : Promise < boolean > {
7558 try {
7659 await spawn ( 'git' , [ 'rev-parse' , '--is-inside-work-tree' ] , {
7760 stdio : 'ignore' ,
7861 cwd : sourceDir ,
7962 } ) ;
63+ return true ;
8064 } catch {
81- // Not a git repository, skip the git clean check
82- return ;
65+ return false ;
8366 }
67+ }
68+
69+ /**
70+ * Gets the list of files that would be removed by git clean
71+ */
72+ async function getFilesToClean (
73+ fingerprintOptions : FingerprintSources ,
74+ platformName : string ,
75+ sourceDir : string ,
76+ ) : Promise < string [ ] > {
77+ if ( ! ( await isGitRepository ( sourceDir ) ) ) {
78+ return [ ] ;
79+ }
80+
8481 const projectRoot = getProjectRoot ( ) ;
8582 const ignorePaths = [
8683 ...( fingerprintOptions ?. ignorePaths ?? [ ] ) ,
@@ -90,21 +87,61 @@ async function warnIgnoredFiles(
9087 projectRoot ,
9188 ) ,
9289 ] ;
90+
9391 const { output } = await spawn ( 'git' , [
9492 'clean' ,
9593 '-fdx' ,
9694 '--dry-run' ,
9795 sourceDir ,
9896 ...ignorePaths . flatMap ( ( path ) => [ '-e' , `${ path } ` ] ) ,
9997 ] ) ;
100- const ignoredFiles = output
98+
99+ return output
101100 . split ( '\n' )
102101 . map ( ( line ) => line . replace ( 'Would remove ' , '' ) )
103102 . filter ( ( line ) => line !== '' ) ;
103+ }
104+
105+ /**
106+ * Executes git clean to remove files
107+ */
108+ async function executeGitClean (
109+ fingerprintOptions : FingerprintSources ,
110+ platformName : string ,
111+ sourceDir : string ,
112+ ) : Promise < void > {
113+ if ( ! ( await isGitRepository ( sourceDir ) ) ) {
114+ throw new Error ( 'Not a git repository' ) ;
115+ }
104116
105- if ( ignoredFiles . length > 0 ) {
117+ const projectRoot = getProjectRoot ( ) ;
118+ const ignorePaths = [
119+ ...( fingerprintOptions ?. ignorePaths ?? [ ] ) ,
120+ ...getAllIgnorePaths (
121+ platformName ,
122+ path . relative ( projectRoot , sourceDir ) , // git expects relative paths
123+ projectRoot ,
124+ ) ,
125+ ] ;
126+
127+ await spawn ( 'git' , [
128+ 'clean' ,
129+ '-fdx' ,
130+ sourceDir ,
131+ ...ignorePaths . flatMap ( ( path ) => [ '-e' , `${ path } ` ] ) ,
132+ ] ) ;
133+ }
134+
135+ async function warnIgnoredFiles (
136+ fingerprintOptions : FingerprintSources ,
137+ platformName : string ,
138+ sourceDir : string ,
139+ ) {
140+ const filesToClean = await getFilesToClean ( fingerprintOptions , platformName , sourceDir ) ;
141+
142+ if ( filesToClean . length > 0 ) {
106143 logger . warn ( `There are files that likely affect fingerprint:
107- ${ ignoredFiles . map ( ( file ) => `- ${ color . bold ( file ) } ` ) . join ( '\n' ) }
144+ ${ filesToClean . map ( ( file ) => `- ${ color . bold ( file ) } ` ) . join ( '\n' ) }
108145Consider removing them or update ${ color . bold (
109146 'fingerprint.ignorePaths' ,
110147 ) } in ${ colorLink ( 'rock.config.mjs' ) } :
@@ -113,3 +150,163 @@ Read more: ${colorLink(
113150 ) } `) ;
114151 }
115152}
153+
154+ /**
155+ * Tries to fetch cached build with optional debugging workflow
156+ */
157+ async function tryFetchCachedBuild ( {
158+ artifactName,
159+ remoteCacheProvider,
160+ fingerprintOptions,
161+ platformName,
162+ sourceDir,
163+ } : {
164+ artifactName : string ;
165+ remoteCacheProvider : null | ( ( ) => RemoteBuildCache ) | undefined ;
166+ fingerprintOptions : FingerprintSources ;
167+ platformName : string ;
168+ sourceDir : string ;
169+ } ) : Promise < string | undefined > {
170+ try {
171+ const cachedBuild = await fetchCachedBuild ( {
172+ artifactName,
173+ remoteCacheProvider,
174+ } ) ;
175+ if ( cachedBuild ) {
176+ return cachedBuild . binaryPath ;
177+ }
178+ } catch ( error ) {
179+ const message = ( error as RockError ) . message ;
180+ const cause = ( error as RockError ) . cause ;
181+ logger . warn (
182+ `Remote Cache: Failed to fetch cached build for ${ color . bold (
183+ artifactName ,
184+ ) } .
185+ Cause: ${ message } ${ cause ? `\n${ cause . toString ( ) } ` : '' }
186+ Read more: ${ colorLink (
187+ 'https://rockjs.dev/docs/configuration#remote-cache-configuration' ,
188+ ) } `,
189+ ) ;
190+
191+ // Check if user has used remote cache before and offer debugging
192+ if ( isInteractive ( ) && hasUsedRemoteCacheBefore ( ) ) {
193+ const cleanedAndRetried = await runCacheMissDebugging ( {
194+ fingerprintOptions,
195+ platformName,
196+ sourceDir,
197+ artifactName,
198+ remoteCacheProvider,
199+ } ) ;
200+
201+ if ( cleanedAndRetried ) {
202+ return cleanedAndRetried ;
203+ }
204+ }
205+
206+ await warnIgnoredFiles ( fingerprintOptions , platformName , sourceDir ) ;
207+ logger . debug ( 'Remote cache failure error:' , error ) ;
208+ logger . info ( 'Continuing with local build' ) ;
209+ }
210+
211+ return undefined ;
212+ }
213+
214+ /**
215+ * Runs the cache miss debugging workflow and returns binary path if successful
216+ */
217+ async function runCacheMissDebugging ( {
218+ fingerprintOptions,
219+ platformName,
220+ sourceDir,
221+ artifactName,
222+ remoteCacheProvider,
223+ } : {
224+ fingerprintOptions : FingerprintSources ;
225+ platformName : string ;
226+ sourceDir : string ;
227+ artifactName : string ;
228+ remoteCacheProvider : null | ( ( ) => RemoteBuildCache ) | undefined ;
229+ } ) : Promise < string | undefined > {
230+ logger . info ( '' ) ; // Add spacing
231+ const shouldDebug = await promptConfirm ( {
232+ message : `Would you like to debug this remote cache miss?` ,
233+ confirmLabel : 'Yes, help me debug this' ,
234+ cancelLabel : 'No, continue with local build' ,
235+ } ) ;
236+
237+ if ( ! shouldDebug ) {
238+ return undefined ;
239+ }
240+
241+ // Step 1: Check what files would be cleaned
242+ const filesToClean = await getFilesToClean ( fingerprintOptions , platformName , sourceDir ) ;
243+
244+ if ( filesToClean . length === 0 ) {
245+ logger . info ( '✅ No files found that would affect fingerprinting.' ) ;
246+ // TODO: backlink to the docs here instead of a 404
247+ logger . info ( ' The cache miss might be due to other factors (CI environment, etc.)' ) ;
248+ return undefined ;
249+ }
250+
251+ // Step 2: Show user what would be cleaned and offer to clean
252+ logger . info ( `📋 Found ${ color . bold ( filesToClean . length . toString ( ) ) } files that affect cache fingerprint:` ) ;
253+ filesToClean . slice ( 0 , 10 ) . forEach ( file => {
254+ logger . info ( ` - ${ color . bold ( file ) } ` ) ;
255+ } ) ;
256+
257+ if ( filesToClean . length > 10 ) {
258+ logger . info ( ` ... and ${ filesToClean . length - 10 } more files` ) ;
259+ }
260+ logger . info ( '' ) ; // Add spacing
261+
262+ const shouldClean = await promptConfirm ( {
263+ message : `Clean these files and retry fetching?` ,
264+ confirmLabel : 'Yes, clean files and retry' ,
265+ cancelLabel : 'No, continue with local build' ,
266+ } ) ;
267+
268+ if ( ! shouldClean ) {
269+ return undefined ;
270+ }
271+
272+ // Step 3: Clean files first
273+ logger . info ( '🧹 Cleaning files...' ) ;
274+ try {
275+ await executeGitClean ( fingerprintOptions , platformName , sourceDir ) ;
276+ logger . info ( '✅ Files cleaned successfully' ) ;
277+ logger . info ( '' ) ; // Add spacing
278+ } catch ( error ) {
279+ logger . error ( `❌ Failed to clean files: ${ error } ` ) ;
280+ logger . info ( ' Continuing with local build...' ) ;
281+ return undefined ;
282+ }
283+
284+ // Extract platform and traits from the original artifact name to recalculate
285+ const projectRoot = getProjectRoot ( ) ;
286+ const nameParts = artifactName . split ( '-' ) ;
287+ const platform = nameParts [ 1 ] as 'ios' | 'android' | 'harmony' ;
288+ const traits = nameParts . slice ( 2 , - 1 ) ; // Everything except 'rock', platform, and hash
289+
290+ const cleanArtifactName = await formatArtifactName ( {
291+ platform,
292+ traits,
293+ root : projectRoot ,
294+ fingerprintOptions,
295+ } ) ;
296+
297+ // Step 5: Retry the fetch with the correct artifact name
298+ logger . info ( '🔄 Retrying remote cache with clean fingerprint...' ) ;
299+
300+ const cachedBuild = await fetchCachedBuild ( {
301+ artifactName : cleanArtifactName ,
302+ remoteCacheProvider,
303+ } ) ;
304+
305+ if ( cachedBuild ) {
306+ logger . info ( '✅ Successfully fetched from remote cache after cleaning!' ) ;
307+ return cachedBuild . binaryPath ;
308+ } else {
309+ logger . info ( '❌ Remote cache still missed after cleaning. Continuing with local build...' ) ;
310+ return undefined ;
311+ }
312+ }
0 commit comments