diff --git a/.github/workflow-scripts/__tests__/firebaseUtils-test.js b/.github/workflow-scripts/__tests__/firebaseUtils-test.js index e8b4b13..2bb322e 100644 --- a/.github/workflow-scripts/__tests__/firebaseUtils-test.js +++ b/.github/workflow-scripts/__tests__/firebaseUtils-test.js @@ -79,7 +79,7 @@ describe('FirebaseClient', () => { password: 'testpassword', returnSecureToken: true, }), - }, + } ); }); @@ -88,7 +88,7 @@ describe('FirebaseClient', () => { const client = new FirebaseClient(); await expect(client.authenticate()).rejects.toThrow( - 'Firebase credentials not found in environment variables', + 'Firebase credentials not found in environment variables' ); }); @@ -97,7 +97,7 @@ describe('FirebaseClient', () => { const client = new FirebaseClient(); await expect(client.authenticate()).rejects.toThrow( - 'Firebase credentials not found in environment variables', + 'Firebase credentials not found in environment variables' ); }); @@ -107,22 +107,22 @@ describe('FirebaseClient', () => { status: 400, text: jest.fn().mockResolvedValueOnce( JSON.stringify({ - error: {message: 'Invalid credentials'}, - }), + error: { message: 'Invalid credentials' }, + }) ), }); const client = new FirebaseClient(); await expect(client.authenticate()).rejects.toThrow( - 'HTTP 400: Invalid credentials', + 'HTTP 400: Invalid credentials' ); }); }); describe('makeRequest', () => { it('should make successful GET request', async () => { - const mockData = {test: 'data'}; + const mockData = { test: 'data' }; global.fetch.mockResolvedValueOnce({ ok: true, text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)), @@ -141,8 +141,8 @@ describe('FirebaseClient', () => { }); it('should make successful POST request with data', async () => { - const mockData = {success: true}; - const postData = {test: 'post data'}; + const mockData = { success: true }; + const postData = { test: 'post data' }; global.fetch.mockResolvedValueOnce({ ok: true, @@ -154,7 +154,7 @@ describe('FirebaseClient', () => { 'example.com', '/test', 'POST', - postData, + postData ); expect(result).toEqual(mockData); @@ -186,15 +186,15 @@ describe('FirebaseClient', () => { status: 404, text: jest.fn().mockResolvedValueOnce( JSON.stringify({ - error: {message: 'Not found'}, - }), + error: { message: 'Not found' }, + }) ), }); const client = new FirebaseClient(); await expect( - client.makeRequest('example.com', '/test', 'GET'), + client.makeRequest('example.com', '/test', 'GET') ).rejects.toThrow('HTTP 404: Not found'); }); @@ -208,14 +208,14 @@ describe('FirebaseClient', () => { const client = new FirebaseClient(); await expect( - client.makeRequest('example.com', '/test', 'GET'), + client.makeRequest('example.com', '/test', 'GET') ).rejects.toThrow('HTTP 500: Internal Server Error'); }); }); describe('makeDatabaseRequest', () => { it('should make database request with existing token', async () => { - const mockData = {test: 'data'}; + const mockData = { test: 'data' }; global.fetch.mockResolvedValueOnce({ ok: true, text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockData)), @@ -234,13 +234,13 @@ describe('FirebaseClient', () => { headers: { 'Content-Type': 'application/json', }, - }, + } ); }); it('should authenticate before making request if no token exists', async () => { - const authResponse = {idToken: 'new-token'}; - const dataResponse = {test: 'data'}; + const authResponse = { idToken: 'new-token' }; + const dataResponse = { test: 'data' }; global.fetch .mockResolvedValueOnce({ @@ -268,7 +268,7 @@ describe('FirebaseClient', () => { const client = new FirebaseClient(); client.idToken = 'existing-token'; - const testData = [{library: 'test', status: 'success'}]; + const testData = [{ library: 'test', status: 'success' }]; await client.makeDatabaseRequest('2023-12-01', 'PUT', testData); @@ -280,7 +280,7 @@ describe('FirebaseClient', () => { 'Content-Type': 'application/json', }, body: JSON.stringify(testData), - }, + } ); }); }); @@ -294,7 +294,7 @@ describe('FirebaseClient', () => { const client = new FirebaseClient(); client.idToken = 'existing-token'; - const results = [{library: 'test', status: 'success'}]; + const results = [{ library: 'test', status: 'success' }]; await client.storeResults('2023-12-01', results); @@ -306,17 +306,17 @@ describe('FirebaseClient', () => { 'Content-Type': 'application/json', }, body: JSON.stringify(results), - }, + } ); expect(console.log).toHaveBeenCalledWith( - 'Successfully stored results for 2023-12-01', + 'Successfully stored results for 2023-12-01' ); }); }); describe('getResults', () => { it('should retrieve results successfully', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; global.fetch.mockResolvedValueOnce({ ok: true, text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), @@ -356,15 +356,15 @@ describe('FirebaseClient', () => { client.idToken = 'existing-token'; await expect(client.getResults('2023-12-01')).rejects.toThrow( - 'HTTP 500: Internal Server Error', + 'HTTP 500: Internal Server Error' ); }); }); describe('getLatestResults', () => { it('should authenticate before making requests if no token exists', async () => { - const authResponse = {idToken: 'new-token'}; - const mockResults = [{library: 'test', status: 'success'}]; + const authResponse = { idToken: 'new-token' }; + const mockResults = [{ library: 'test', status: 'success' }]; global.fetch .mockResolvedValueOnce({ @@ -385,15 +385,15 @@ describe('FirebaseClient', () => { }); expect(client.idToken).toBe('new-token'); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', + 'Checking for results on 2023-12-14 (1 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-14 (1 days back)', + 'Found results from 2023-12-14 (1 days back)' ); }); it('should find results from the previous day', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; global.fetch.mockResolvedValueOnce({ ok: true, text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), @@ -409,15 +409,15 @@ describe('FirebaseClient', () => { date: '2023-12-14', }); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', + 'Checking for results on 2023-12-14 (1 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-14 (1 days back)', + 'Found results from 2023-12-14 (1 days back)' ); }); it('should find results from several days back', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; // Mock 404 responses for first 2 days, then success on 3rd day global.fetch @@ -446,21 +446,21 @@ describe('FirebaseClient', () => { date: '2023-12-12', }); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', + 'Checking for results on 2023-12-14 (1 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-13 (2 days back)...', + 'Checking for results on 2023-12-13 (2 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-12 (3 days back)...', + 'Checking for results on 2023-12-12 (3 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-12 (3 days back)', + 'Found results from 2023-12-12 (3 days back)' ); }); it('should skip empty results and continue searching', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; // Mock empty array for first day, then valid results on second day global.fetch @@ -483,13 +483,13 @@ describe('FirebaseClient', () => { date: '2023-12-13', }); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-14 (1 days back)...', + 'Checking for results on 2023-12-14 (1 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-13 (2 days back)...', + 'Checking for results on 2023-12-13 (2 days back)...' ); expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-13 (2 days back)', + 'Found results from 2023-12-13 (2 days back)' ); }); @@ -511,7 +511,7 @@ describe('FirebaseClient', () => { date: null, }); expect(console.log).toHaveBeenCalledWith( - 'No previous results found within the last 3 days', + 'No previous results found within the last 3 days' ); expect(global.fetch).toHaveBeenCalledTimes(3); }); @@ -534,13 +534,13 @@ describe('FirebaseClient', () => { date: null, }); expect(console.log).toHaveBeenCalledWith( - 'No previous results found within the last 7 days', + 'No previous results found within the last 7 days' ); expect(global.fetch).toHaveBeenCalledTimes(7); }); it('should handle non-404 errors and continue searching', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; // Mock 500 error for first day, then success on second day global.fetch @@ -564,15 +564,15 @@ describe('FirebaseClient', () => { date: '2023-12-13', }); expect(console.log).toHaveBeenCalledWith( - 'No results found for 2023-12-14: HTTP 500: Internal Server Error', + 'No results found for 2023-12-14: HTTP 500: Internal Server Error' ); expect(console.log).toHaveBeenCalledWith( - 'Found results from 2023-12-13 (2 days back)', + 'Found results from 2023-12-13 (2 days back)' ); }); it('should handle date boundaries correctly', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; global.fetch.mockResolvedValueOnce({ ok: true, text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), @@ -589,12 +589,12 @@ describe('FirebaseClient', () => { date: '2023-11-30', }); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-11-30 (1 days back)...', + 'Checking for results on 2023-11-30 (1 days back)...' ); }); it('should handle year boundary correctly', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; global.fetch.mockResolvedValueOnce({ ok: true, text: jest.fn().mockResolvedValueOnce(JSON.stringify(mockResults)), @@ -611,12 +611,12 @@ describe('FirebaseClient', () => { date: '2023-12-31', }); expect(console.log).toHaveBeenCalledWith( - 'Checking for results on 2023-12-31 (1 days back)...', + 'Checking for results on 2023-12-31 (1 days back)...' ); }); it('should handle null results and continue searching', async () => { - const mockResults = [{library: 'test', status: 'success'}]; + const mockResults = [{ library: 'test', status: 'success' }]; // Mock null for first day, then valid results on second day global.fetch @@ -645,8 +645,8 @@ describe('FirebaseClient', () => { describe('compareResults', () => { it('should handle null previous results', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const result = compareResults(currentResults, null); @@ -654,13 +654,13 @@ describe('compareResults', () => { expect(result).toEqual({ broken: [], recovered: [], - newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}], + newFailures: [{ library: 'lib1', platform: 'iOS', status: 'failed' }], }); }); it('should handle undefined previous results', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, ]; const result = compareResults(currentResults, undefined); @@ -668,19 +668,19 @@ describe('compareResults', () => { expect(result).toEqual({ broken: [], recovered: [], - newFailures: [{library: 'lib1', platform: 'iOS', status: 'failed'}], + newFailures: [{ library: 'lib1', platform: 'iOS', status: 'failed' }], }); }); it('should identify broken tests', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'success' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const result = compareResults(currentResults, previousResults); @@ -698,13 +698,13 @@ describe('compareResults', () => { it('should identify recovered tests', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'success' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const result = compareResults(currentResults, previousResults); @@ -722,15 +722,15 @@ describe('compareResults', () => { it('should identify both broken and recovered tests', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, - {library: 'lib3', platform: 'iOS', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, + { library: 'lib2', platform: 'Android', status: 'success' }, + { library: 'lib3', platform: 'iOS', status: 'success' }, ]; const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'failed'}, - {library: 'lib3', platform: 'iOS', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'success' }, + { library: 'lib2', platform: 'Android', status: 'failed' }, + { library: 'lib3', platform: 'iOS', status: 'success' }, ]; const result = compareResults(currentResults, previousResults); @@ -755,12 +755,12 @@ describe('compareResults', () => { it('should handle tests that are not in previous results', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'success' }, ]; const result = compareResults(currentResults, previousResults); @@ -779,7 +779,7 @@ describe('compareResults', () => { it('should handle empty current results', () => { const currentResults = []; const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'success' }, ]; const result = compareResults(currentResults, previousResults); @@ -790,7 +790,7 @@ describe('compareResults', () => { it('should handle empty previous results', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'failed'}, + { library: 'lib1', platform: 'iOS', status: 'failed' }, ]; const previousResults = []; @@ -805,13 +805,13 @@ describe('compareResults', () => { it('should handle different status values', () => { const currentResults = [ - {library: 'lib1', platform: 'iOS', status: 'timeout'}, - {library: 'lib2', platform: 'Android', status: 'success'}, + { library: 'lib1', platform: 'iOS', status: 'timeout' }, + { library: 'lib2', platform: 'Android', status: 'success' }, ]; const previousResults = [ - {library: 'lib1', platform: 'iOS', status: 'success'}, - {library: 'lib2', platform: 'Android', status: 'error'}, + { library: 'lib1', platform: 'iOS', status: 'success' }, + { library: 'lib2', platform: 'Android', status: 'error' }, ]; const result = compareResults(currentResults, previousResults); @@ -892,4 +892,4 @@ describe('getTodayDate', () => { global.Date.mockRestore(); }); -}); \ No newline at end of file +}); diff --git a/.github/workflow-scripts/__tests__/notifyDiscord-test.js b/.github/workflow-scripts/__tests__/notifyDiscord-test.js index 8b88615..ad90f3c 100644 --- a/.github/workflow-scripts/__tests__/notifyDiscord-test.js +++ b/.github/workflow-scripts/__tests__/notifyDiscord-test.js @@ -122,7 +122,7 @@ describe('sendMessageToDiscord', () => { it('should throw an error if webhook URL is missing', async () => { await expect(sendMessageToDiscord(null, {})).rejects.toThrow( - 'Discord webhook URL is missing', + 'Discord webhook URL is missing' ); }); @@ -134,7 +134,7 @@ describe('sendMessageToDiscord', () => { }); const webhook = 'https://discord.com/api/webhooks/123/abc'; - const message = {content: 'Test message'}; + const message = { content: 'Test message' }; await expect(sendMessageToDiscord(webhook, message)).resolves.not.toThrow(); @@ -149,7 +149,7 @@ describe('sendMessageToDiscord', () => { // Verify console.log was called expect(console.log).toHaveBeenCalledWith( - 'Successfully sent message to Discord', + 'Successfully sent message to Discord' ); }); @@ -162,15 +162,15 @@ describe('sendMessageToDiscord', () => { }); const webhook = 'https://discord.com/api/webhooks/123/abc'; - const message = {content: 'Test message'}; + const message = { content: 'Test message' }; await expect(sendMessageToDiscord(webhook, message)).rejects.toThrow( - 'HTTP status code: 400', + 'HTTP status code: 400' ); // Verify console.error was called expect(console.error).toHaveBeenCalledWith( - 'Failed to send message to Discord: 400 Bad Request', + 'Failed to send message to Discord: 400 Bad Request' ); }); @@ -180,10 +180,10 @@ describe('sendMessageToDiscord', () => { global.fetch.mockRejectedValueOnce(networkError); const webhook = 'https://discord.com/api/webhooks/123/abc'; - const message = {content: 'Test message'}; + const message = { content: 'Test message' }; await expect(sendMessageToDiscord(webhook, message)).rejects.toThrow( - 'Network error', + 'Network error' ); }); -}); \ No newline at end of file +}); diff --git a/.github/workflow-scripts/collectNightlyOutcomes.js b/.github/workflow-scripts/collectNightlyOutcomes.js index ff0d37f..cb7d83a 100644 --- a/.github/workflow-scripts/collectNightlyOutcomes.js +++ b/.github/workflow-scripts/collectNightlyOutcomes.js @@ -7,18 +7,19 @@ * @format */ -const fs = require('fs'); -const path = require('path'); -const { - prepareFailurePayload, - prepareComparisonPayload, - sendMessageToDiscord, -} = require('./notifyDiscord'); +const fs = require('node:fs'); +const path = require('node:path'); + const { FirebaseClient, compareResults, getTodayDate, } = require('./firebaseUtils'); +const { + prepareFailurePayload, + prepareComparisonPayload, + sendMessageToDiscord, +} = require('./notifyDiscord'); function readOutcomes() { const baseDir = '/tmp'; @@ -29,19 +30,21 @@ function readOutcomes() { fs.readdirSync(fullPath).forEach(subFile => { const subFullPath = path.join(fullPath, subFile); if (subFullPath.endsWith('outcome')) { - const [library, status, url] = String(fs.readFileSync(subFullPath, 'utf8')) + const [library, status, url] = String( + fs.readFileSync(subFullPath, 'utf8') + ) .trim() .split('|'); const platform = subFile.includes('android') ? 'Android' : 'iOS'; const runUrl = status.trim() === 'failure' ? url : undefined; console.log( - `[${platform}] ${library} completed with status ${status}`, + `[${platform}] ${library} completed with status ${status}` ); outcomes.push({ library: library.trim(), platform, status: status.trim(), - runUrl + runUrl, }); } }); @@ -56,7 +59,7 @@ function readOutcomes() { library: library.trim(), platform, status: status.trim(), - runUrl + runUrl, }); } }); @@ -69,7 +72,7 @@ function printFailures(outcomes) { outcomes.forEach(entry => { if (entry.status !== 'success') { console.log( - `❌ [${entry.platform}] ${entry.library} failed with status ${entry.status}`, + `❌ [${entry.platform}] ${entry.library} failed with status ${entry.status}` ); failedLibraries.push({ library: entry.library, @@ -127,7 +130,7 @@ async function collectResults(discordWebHook) { // Get the most recent previous results for comparison console.log(`Looking for most recent previous results before ${today}...`); - const {results: previousResults, date: previousDate} = + const { results: previousResults, date: previousDate } = await firebaseClient.getLatestResults(today); let broken = []; @@ -141,11 +144,11 @@ async function collectResults(discordWebHook) { recovered = comparison.recovered; console.log( - `Found ${broken.length} newly broken jobs and ${recovered.length} recovered jobs compared to ${previousDate}`, + `Found ${broken.length} newly broken jobs and ${recovered.length} recovered jobs compared to ${previousDate}` ); } else { console.log( - 'No previous results found for comparison - this might be the first run or no recent data available', + 'No previous results found for comparison - this might be the first run or no recent data available' ); } diff --git a/.github/workflow-scripts/firebaseUtils.js b/.github/workflow-scripts/firebaseUtils.js index f19d68e..dd92ca9 100644 --- a/.github/workflow-scripts/firebaseUtils.js +++ b/.github/workflow-scripts/firebaseUtils.js @@ -22,7 +22,7 @@ class FirebaseClient { async authenticate() { if (!this.email || !this.password) { throw new Error( - 'Firebase credentials not found in environment variables', + 'Firebase credentials not found in environment variables' ); } @@ -36,11 +36,10 @@ class FirebaseClient { 'identitytoolkit.googleapis.com', `/v1/accounts:signInWithPassword?key=${this.apiKey}`, 'POST', - authData, + authData ); this.idToken = response.idToken; - return; } /** @@ -105,16 +104,16 @@ class FirebaseClient { const checkDateStr = checkDate.toISOString().split('T')[0]; console.log( - `Checking for results on ${checkDateStr} (${daysBack} days back)...`, + `Checking for results on ${checkDateStr} (${daysBack} days back)...` ); try { const results = await this.getResults(checkDateStr); if (results && results.length > 0) { console.log( - `Found results from ${checkDateStr} (${daysBack} days back)`, + `Found results from ${checkDateStr} (${daysBack} days back)` ); - return {results, date: checkDateStr}; + return { results, date: checkDateStr }; } } catch (error) { console.log(`No results found for ${checkDateStr}: ${error.message}`); @@ -123,9 +122,9 @@ class FirebaseClient { } console.log( - `No previous results found within the last ${maxDaysBack} days`, + `No previous results found within the last ${maxDaysBack} days` ); - return {results: null, date: null}; + return { results: null, date: null }; } async makeRequest(hostname, path, method, data = null) { @@ -231,7 +230,7 @@ function compareResults(currentResults, previousResults) { } } - return {broken, recovered}; + return { broken, recovered }; } /** diff --git a/.github/workflow-scripts/notifyDiscord.js b/.github/workflow-scripts/notifyDiscord.js index f05d9f5..c4f93b6 100644 --- a/.github/workflow-scripts/notifyDiscord.js +++ b/.github/workflow-scripts/notifyDiscord.js @@ -34,7 +34,7 @@ async function sendMessageToDiscord(webHook, message) { } else { const errorText = await response.text(); console.error( - `Failed to send message to Discord: ${response.status} ${errorText}`, + `Failed to send message to Discord: ${response.status} ${errorText}` ); throw new Error(`HTTP status code: ${response.status}`); } @@ -125,7 +125,7 @@ function prepareComparisonPayload(broken, recovered) { } } - return {content}; + return { content }; } // Export the functions using CommonJS syntax diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 941c74c..477a7db 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -19,6 +19,8 @@ jobs: node-version: 24 cache: 'yarn' - name: Install deps - run: npm install + run: yarn install --frozen-lockfile + - name: Lint code + run: yarn lint - name: Run tests - run: npm test + run: yarn test diff --git a/README.md b/README.md index fb43a4f..bff73dd 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Automated GitHub Actions workflows for testing React Native ecosystem libraries This repository contains GitHub Actions workflows that automatically test **popular React Native OSS libraries** against React Native **nightly builds**. The system runs daily to catch breaking changes early and ensure ecosystem compatibility with upcoming React Native releases. Specifically this repo will: + - Run a daily job **every night at 4:15 AM UTC** via GitHub Actions scheduled workflows - Testing the latest `react-native@nightly` build against a list of popular libraries, such as: - `react-native-async-storage` @@ -20,15 +21,17 @@ Specifically this repo will: - Send a Discord message for failure alerts and status updates - Store results in Firebase for historical tracking and run comparison to identify newly broken or recovered libraries -#### How to apply? +### How to apply? If you're a library maintainer, you can now sign up to be part of our nightly testing to make sure your library will keep on working. Read more in the Discussions and Proposals discussion: -* https://github.com/react-native-community/discussions-and-proposals/discussions/931 + +- https://github.com/react-native-community/discussions-and-proposals/discussions/931 ### Website The test results are also published on the website, which is available on the following address: -* https://react-native-community.github.io/nightly-tests/ + +- https://react-native-community.github.io/nightly-tests/ To learn more about website app, see [the README file](./website/README.md) in the `website` directory. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..a63d8f5 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,84 @@ +import jsPlugin from '@eslint/js'; +import jsonPlugin from '@eslint/json'; +import markdownPlugin from '@eslint/markdown'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import importPlugin from 'eslint-plugin-import'; +import jestPlugin from 'eslint-plugin-jest'; +import prettierPlugin from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; + +export default defineConfig([ + globalIgnores(['website']), + + prettierPlugin, + importPlugin.flatConfigs.recommended, + + { + rules: { + 'prettier/prettier': [ + 'error', + { + arrowParens: 'avoid', + bracketSameLine: true, + printWidth: 80, + singleQuote: true, + trailingComma: 'es5', + endOfLine: 'auto', + }, + ], + }, + }, + + { + files: ['**/*.{js,mjs}'], + plugins: { + js: jsPlugin, + }, + languageOptions: { + globals: globals.node, + ecmaVersion: 'latest', + sourceType: 'module', + }, + extends: ['js/recommended'], + rules: { + 'import/no-unresolved': 'off', + 'import/enforce-node-protocol-usage': ['error', 'always'], + 'import/order': [ + 'error', + { + groups: [['external', 'builtin'], 'internal', ['parent', 'sibling']], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + }, + }, + ], + }, + }, + + { + files: ['**/*-test.js'], + plugins: { jest: jestPlugin }, + languageOptions: { + globals: jestPlugin.environments.globals.globals, + }, + ...jestPlugin.configs['flat/recommended'], + }, + + { + files: ['**/*.json'], + language: 'json/json', + plugins: { + json: jsonPlugin, + }, + extends: ['json/recommended'], + }, + + { + files: ['**/*.md'], + plugins: { + markdown: markdownPlugin, + }, + extends: ['markdown/recommended'], + }, +]); diff --git a/jest.config.js b/jest.config.js index fa7322b..25c9bac 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,3 @@ module.exports = { - testEnvironment: 'node' + testEnvironment: 'node', }; diff --git a/libraries.schema.json b/libraries.schema.json index 21351a6..1f37aed 100644 --- a/libraries.schema.json +++ b/libraries.schema.json @@ -39,9 +39,7 @@ "type": "string" }, "description": "Array of GitHub usernames. We will use it in the future to communicate with library maintainers (e.g. ping for build failures).", - "examples": [ - ["frodo", "bilbo", "pippin", "merry", "sam"] - ] + "examples": [["frodo", "bilbo", "pippin", "merry", "sam"]] }, "notes": { "type": "string", diff --git a/package.json b/package.json index 9da3ad5..a80e3fa 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "description": "Nightly integration tests for React Native", "version": "0.0.0", "private": "true", - "workspaces": ["website"], + "workspaces": [ + "website" + ], "engines": { "node": ">=22" }, @@ -14,11 +16,24 @@ "repository": "github:react-native-community/nightly-tests", "devDependencies": { "@babel/core": "^7.28.5", - "jest": "^27.0.0", + "@eslint/js": "^9.39.1", + "@eslint/json": "^0.14.0", + "@eslint/markdown": "^7.5.1", "babel-jest": "^27.0.0", - "babel-preset-env": "^1.7.0" + "babel-preset-env": "^1.7.0", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jest": "^29.2.1", + "eslint-plugin-prettier": "^5.5.4", + "globals": "^16.5.0", + "jest": "^27.0.0", + "prettier": "^3.7.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.1" }, "scripts": { + "lint": "eslint", "test": "jest --config jest.config.js", "test:watch": "jest --watch --config jest.config.js" }, diff --git a/patch.md b/patch.md index 558d83e..867ba81 100644 --- a/patch.md +++ b/patch.md @@ -29,11 +29,12 @@ node ../../scripts/make-patch.js ../../patches/{libraryName}.patch ``` For example: + ```sh node ../../scripts/make-patch.js ../../patches/react-native-turbo-encryption.patch ``` -This will generate a patch file in the patches folder. +This will generate a patch file in the `patches` folder. **Note:** Remove any lock files like `yarn.lock`, `Podfile.lock`, or any generated files that are tracked before generating the patch file, as these can lead to an excessively long patch file. @@ -42,26 +43,27 @@ This will generate a patch file in the patches folder. ```sh node ../../scripts/apply-patch.js ../../patches/{libraryName}.patch ``` + For example: + ```sh node ../../scripts/apply-patch.js ../../patches/react-native-turbo-encryption.patch ``` - ### Step 6: Add the Patch File to `libraries.json` -```json +```jsonc "react-native-reanimated": { - "description": "React Native's Animated library reimplemented", - "installCommand": "react-native-reanimated@nightly react-native-worklets@nightly", - "android": true, - "ios": true, - "maintainersUsernames": [], - "notes": "", - "patchFile": "patches/reanimated.patch" # <-- Path to patch file - } + "description": "React Native's Animated library reimplemented", + "installCommand": "react-native-reanimated@nightly react-native-worklets@nightly", + "android": true, + "ios": true, + "maintainersUsernames": [], + "notes": "", + "patchFile": "patches/reanimated.patch" # <-- Path to patch file +} ``` ### Step 7: Submit Your Changes -Push the changes and create a pull request. Your patch file is ready! \ No newline at end of file +Push the changes and create a pull request. Your patch file is ready! diff --git a/scripts/apply-patch.js b/scripts/apply-patch.js index 8b820a7..a07d409 100755 --- a/scripts/apply-patch.js +++ b/scripts/apply-patch.js @@ -2,27 +2,27 @@ /** * apply-patch.js * Applies a patch file to the current directory - * + * * Usage: * node scripts/apply-patch.js */ -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); +const { execSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); // Get patch file from arguments const patchFile = process.argv[2]; if (!patchFile) { - console.error("Usage: node scripts/apply-patch.js "); - console.error("Example: node scripts/apply-patch.js patches/my-fix.patch"); + console.error('Usage: node scripts/apply-patch.js '); + console.error('Example: node scripts/apply-patch.js patches/my-fix.patch'); process.exit(1); } try { // Check if we're in a git repository - execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" }); + execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); // Check if patch file exists const patchPath = path.resolve(process.cwd(), patchFile); @@ -35,14 +35,13 @@ try { // Apply the patch execSync(`git apply --binary --3way --whitespace=nowarn "${patchPath}"`, { - stdio: "inherit", - encoding: "utf8" + stdio: 'inherit', + encoding: 'utf8', }); - console.log("✅ Patch applied successfully!"); - + console.log('✅ Patch applied successfully!'); } catch (err) { - console.error("❌ Failed to apply patch"); + console.error('❌ Failed to apply patch'); console.error(err.message); process.exit(1); } diff --git a/scripts/make-patch.js b/scripts/make-patch.js index 9cfcaca..c8f9fec 100755 --- a/scripts/make-patch.js +++ b/scripts/make-patch.js @@ -2,54 +2,55 @@ /** * make-patch.js * Creates a patch file from all current changes (staged and unstaged) - * + * * Usage: * node scripts/make-patch.js [output-filename] - * + * * If no filename is provided, generates one with timestamp. */ -const { execSync } = require("child_process"); -const fs = require("fs"); -const path = require("path"); +const { execSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); // Get output filename from args or generate timestamp-based name -const outputFile = process.argv[2] || (() => { - const d = new Date(); - const pad = n => String(n).padStart(2, "0"); - return `patch-${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}.patch`; -})(); +const outputFile = + process.argv[2] || + (() => { + const d = new Date(); + const pad = n => String(n).padStart(2, '0'); + return `patch-${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}.patch`; + })(); try { // Check if we're in a git repository - execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" }); + execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); // Create diff of all changes (staged + unstaged + untracked) - console.log("Creating patch from all changes..."); - + console.log('Creating patch from all changes...'); + // Stage untracked files temporarily (without content) - execSync("git add -N .", { stdio: "ignore" }); - + execSync('git add -N .', { stdio: 'ignore' }); + // Generate the patch - const patch = execSync("git diff HEAD --binary", { - encoding: "utf8", - maxBuffer: 100 * 1024 * 1024 // 100MB buffer + const patch = execSync('git diff HEAD --binary', { + encoding: 'utf8', + maxBuffer: 100 * 1024 * 1024, // 100MB buffer }); if (!patch.trim()) { - console.log("No changes found. Nothing to patch."); + console.log('No changes found. Nothing to patch.'); process.exit(0); } // Write patch file const outputPath = path.resolve(process.cwd(), outputFile); - fs.writeFileSync(outputPath, patch, "utf8"); + fs.writeFileSync(outputPath, patch, 'utf8'); console.log(`✅ Patch created successfully: ${outputPath}`); console.log(` To apply: node scripts/apply-patch.js ${outputFile}`); - } catch (err) { - console.error("❌ Error creating patch:"); + console.error('❌ Error creating patch:'); console.error(err.message); process.exit(1); } diff --git a/website/README.md b/website/README.md index 6ae4e2c..e5c69fa 100644 --- a/website/README.md +++ b/website/README.md @@ -8,17 +8,17 @@ yarn dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -### Stack +## Stack -* React -* Next.js -* Tailwind +- React +- Next.js +- Tailwind -### Directory Structure +## Directory Structure The high-level overview of relevant files and folders. -``` +```text website/ ├── app/ │ └── [Next App Router] @@ -31,7 +31,7 @@ website/ ├── scripts/ │ │ // Node script for fetching and reformatting latest data from Firebase. │ │ // Requires Firebase Service Key in `.env` file. -│ └── fetch-data.mjs +│ └── fetch-data.mjs ├── styles/ │ │ // Global Styles and Tailwind Setup. │ └── global.css diff --git a/website/app/layout.tsx b/website/app/layout.tsx index 3b70c6b..75a78fa 100644 --- a/website/app/layout.tsx +++ b/website/app/layout.tsx @@ -1,39 +1,39 @@ -import type { Metadata } from "next"; -import { ThemeProvider } from "next-themes"; -import { PropsWithChildren } from "react"; +import type { Metadata } from 'next'; +import { ThemeProvider } from 'next-themes'; +import { PropsWithChildren } from 'react'; -import Footer from "~/components/Footer"; -import Header from "~/components/Header"; -import { SearchProvider } from "~/context/SearchContext"; -import getAssetPath from "~/utils/getAssetPath"; +import Footer from '~/components/Footer'; +import Header from '~/components/Header'; +import { SearchProvider } from '~/context/SearchContext'; +import getAssetPath from '~/utils/getAssetPath'; -import "~/styles/globals.css"; +import '~/styles/globals.css'; const metadataBase = process.env.REPOSITORY_NAME - ? new URL("https://react-native-community.github.io/") - : new URL("http://localhost:3000"); + ? new URL('https://react-native-community.github.io/') + : new URL('http://localhost:3000'); export const metadata: Metadata = { - title: "React Native Nightly Tests", - description: "Nightly integration tests results for React Native", + title: 'React Native Nightly Tests', + description: 'Nightly integration tests results for React Native', metadataBase, icons: { icon: [ { - url: getAssetPath("favicon-32x32.png"), - type: "image/png", - sizes: "32x32", + url: getAssetPath('favicon-32x32.png'), + type: 'image/png', + sizes: '32x32', }, { - url: getAssetPath("/favicon-16x16.png"), - type: "image/png", - sizes: "16x16", + url: getAssetPath('/favicon-16x16.png'), + type: 'image/png', + sizes: '16x16', }, ], - shortcut: getAssetPath("favicon-32x32.png"), + shortcut: getAssetPath('favicon-32x32.png'), }, openGraph: { - images: getAssetPath("opengraph-image.png"), + images: getAssetPath('opengraph-image.png'), }, }; diff --git a/website/app/page.tsx b/website/app/page.tsx index 24427b5..2cb7916 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -1,8 +1,8 @@ -import Link from "next/link"; +import Link from 'next/link'; -import Table from "~/components/Table"; -import AndroidIcon from "~/public/android-icon.svg"; -import IOSIcon from "~/public/ios-icon.svg"; +import Table from '~/components/Table'; +import AndroidIcon from '~/public/android-icon.svg'; +import IOSIcon from '~/public/ios-icon.svg'; export default function Home() { return ( diff --git a/website/components/EntryNotes.tsx b/website/components/EntryNotes.tsx index e9b34d3..2c3678e 100644 --- a/website/components/EntryNotes.tsx +++ b/website/components/EntryNotes.tsx @@ -1,6 +1,6 @@ -import InfoIcon from "~/public/info-icon.svg"; +import InfoIcon from '~/public/info-icon.svg'; -import Tooltip from "./Tooltip"; +import Tooltip from './Tooltip'; type Props = { notes: string }; diff --git a/website/components/Footer.tsx b/website/components/Footer.tsx index bf3b24c..d62a3fd 100644 --- a/website/components/Footer.tsx +++ b/website/components/Footer.tsx @@ -1,15 +1,15 @@ -import Link from "next/link"; -import { twMerge } from "tailwind-merge"; +import Link from 'next/link'; +import { twMerge } from 'tailwind-merge'; -import InlineLink from "~/components/InlineLink"; +import InlineLink from '~/components/InlineLink'; export default function Footer() { return (