-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add @percy/cli update notice (#678)
- Loading branch information
Wil Wilsman
authored
Dec 16, 2021
1 parent
5464cc6
commit 7dff315
Showing
7 changed files
with
267 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { default, percy } from './percy'; | ||
export { checkForUpdate } from './update'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import logger from '@percy/logger'; | ||
import { colors } from '@percy/logger/dist/util'; | ||
import pkg from '../package.json'; | ||
|
||
// filepath where the cache will be read and written to | ||
const CACHE_FILE = path.join(__dirname, '..', '.releases'); | ||
// max age the cache should be used for (3 days) | ||
const CACHE_MAX_AGE = 3 * 24 * 60 * 60 * 1000; | ||
|
||
// Safely read from CACHE_FILE and return an object containing `data` mirroring what was previously | ||
// written using `writeToCache(data)`. An empty object is returned when older than CACHE_MAX_AGE, | ||
// and an `error` will be present if one was encountered. | ||
function readFromCache() { | ||
let cached = {}; | ||
|
||
try { | ||
if (fs.existsSync(CACHE_FILE)) { | ||
let { createdAt, data } = JSON.parse(fs.readFileSync(CACHE_FILE)); | ||
if ((Date.now() - createdAt) < CACHE_MAX_AGE) cached.data = data; | ||
} | ||
} catch (error) { | ||
let log = logger('cli:update:cache'); | ||
log.debug('Unable to read from cache'); | ||
log.debug(cached.error = error); | ||
} | ||
|
||
return cached; | ||
} | ||
|
||
// Safely write data to CACHE_FILE with the current timestamp. | ||
function writeToCache(data) { | ||
try { | ||
fs.writeFileSync(CACHE_FILE, JSON.stringify({ | ||
createdAt: Date.now(), | ||
data | ||
})); | ||
} catch (error) { | ||
let log = logger('cli:update:cache'); | ||
log.debug('Unable to write to cache'); | ||
log.debug(error); | ||
} | ||
} | ||
|
||
// Fetch and return release information for @percy/cli. | ||
async function fetchReleases() { | ||
let { request } = await import('@percy/client/dist/request'); | ||
|
||
// fetch releases from the github api without retries | ||
let api = 'https://api.github.com/repos/percy/cli/releases'; | ||
let data = await request(api, { | ||
headers: { 'User-Agent': pkg.name }, | ||
retries: 0 | ||
}); | ||
|
||
// return relevant information | ||
return data.map(r => ({ | ||
tag: r.tag_name, | ||
prerelease: r.prerelease | ||
})); | ||
} | ||
|
||
// Check for updates by comparing latest releases with the current version. The result of the check | ||
// is cached to speed up subsequent CLI usage. | ||
export async function checkForUpdate() { | ||
let { data: releases, error: cacheError } = readFromCache(); | ||
let log = logger('cli:update'); | ||
|
||
try { | ||
// request new release information if needed | ||
if (!releases) { | ||
releases = await fetchReleases(); | ||
if (!cacheError) writeToCache(releases, log); | ||
} | ||
|
||
// check the current package version against released versions | ||
let versions = releases.map(r => r.tag.substr(1)); | ||
let age = versions.indexOf(pkg.version); | ||
|
||
// a new version is available | ||
if (age !== 0) { | ||
let range = `${colors.red(pkg.version)} -> ${colors.green(versions[0])}`; | ||
|
||
log.stderr.write('\n'); | ||
log.warn(`${age > 0 && age < 10 ? 'A new version of @percy/cli is available!' : ( | ||
'Heads up! The current version of @percy/cli is more than 10 releases behind!' | ||
)} ${range}`); | ||
log.stderr.write('\n'); | ||
} | ||
} catch (err) { | ||
log.debug('Unable to check for updates'); | ||
log.debug(err); | ||
} | ||
} | ||
|
||
export default checkForUpdate; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import nock from 'nock'; | ||
import logger from '@percy/logger/test/helpers'; | ||
|
||
import { | ||
mockfs, | ||
mockRequire, | ||
mockUpdateCache | ||
} from './helpers'; | ||
|
||
describe('CLI update check', () => { | ||
let checkForUpdate, request; | ||
|
||
beforeEach(async () => { | ||
mockfs(); | ||
logger.mock(); | ||
|
||
request = nock('https://api.github.com/repos/percy/cli', { | ||
reqheaders: { 'User-Agent': ua => !!ua } | ||
}); | ||
|
||
mockRequire('../package.json', { name: '@percy/cli', version: '1.0.0' }); | ||
({ checkForUpdate } = mockRequire.reRequire('../src/update')); | ||
}); | ||
|
||
afterEach(() => { | ||
mockfs.reset(); | ||
nock.cleanAll(); | ||
}); | ||
|
||
it('fetches and caches the latest release information', async () => { | ||
request.get('/releases').reply(200, [{ tag_name: 'v1.0.0' }]); | ||
|
||
expect(mockfs.existsSync('.releases')).toBe(false); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([]); | ||
expect(request.isDone()).toBe(true); | ||
|
||
expect(mockfs.existsSync('.releases')).toBe(true); | ||
expect(JSON.parse(mockfs.readFileSync('.releases'))) | ||
.toHaveProperty('data', [{ tag: 'v1.0.0' }]); | ||
}); | ||
|
||
it('does not fetch the latest release information if cached', async () => { | ||
request.get('/releases').reply(200, [{ tag_name: 'v1.0.0' }]); | ||
mockUpdateCache([{ tag: 'v1.0.0' }]); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([]); | ||
expect(request.isDone()).toBe(false); | ||
}); | ||
|
||
it('fetchs the latest release information if the cache is outdated', async () => { | ||
request.get('/releases').reply(200, [{ tag_name: 'v1.0.0' }]); | ||
|
||
let cacheCreatedAt = Date.now() - (30 * 24 * 60 * 60 * 1000); | ||
mockUpdateCache([{ tag: 'v0.2.0' }, { tag: 'v0.1.0' }], cacheCreatedAt); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([]); | ||
expect(request.isDone()).toBe(true); | ||
|
||
expect(JSON.parse(mockfs.readFileSync('.releases'))) | ||
.toHaveProperty('data', [{ tag: 'v1.0.0' }]); | ||
}); | ||
|
||
it('warns when a new version is available', async () => { | ||
mockUpdateCache([{ tag: 'v1.1.0' }, { tag: 'v1.0.0' }]); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([ | ||
'', '[percy] A new version of @percy/cli is available! 1.0.0 -> 1.1.0', '' | ||
]); | ||
}); | ||
|
||
it('warns when the current version is outdated', async () => { | ||
mockUpdateCache([{ tag: 'v2.0.0' }]); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([ | ||
'', '[percy] Heads up! The current version of @percy/cli ' + | ||
'is more than 10 releases behind! 1.0.0 -> 2.0.0', '' | ||
]); | ||
}); | ||
|
||
it('handles errors reading from cache and logs debug info', async () => { | ||
mockUpdateCache([{ tag: 'v1.0.0' }]); | ||
mockfs.spyOn('readFileSync').and.throwError(new Error('EACCES')); | ||
request.get('/releases').reply(200, [{ tag_name: 'v1.0.0' }]).persist(); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([]); | ||
|
||
logger.loglevel('debug'); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([ | ||
'[percy:cli:update:cache] Unable to read from cache', | ||
jasmine.stringContaining('[percy:cli:update:cache] Error: EACCES') | ||
]); | ||
|
||
expect(request.isDone()).toEqual(true); | ||
}); | ||
|
||
it('handles errors writing to cache and logs debug info', async () => { | ||
mockfs.spyOn('writeFileSync').and.throwError(new Error('EACCES')); | ||
request.get('/releases').reply(200, [{ tag_name: 'v1.0.0' }]).persist(); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([]); | ||
|
||
logger.loglevel('debug'); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([ | ||
'[percy:cli:update:cache] Unable to write to cache', | ||
jasmine.stringContaining('[percy:cli:update:cache] Error: EACCES') | ||
]); | ||
|
||
expect(request.isDone()).toEqual(true); | ||
expect(mockfs.existsSync('.releases')).toBe(false); | ||
}); | ||
|
||
it('handles request errors and logs debug info', async () => { | ||
request.get('/releases').reply(503).persist(); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([]); | ||
|
||
logger.loglevel('debug'); | ||
|
||
await checkForUpdate(); | ||
expect(logger.stdout).toEqual([]); | ||
expect(logger.stderr).toEqual([ | ||
'[percy:cli:update] Unable to check for updates', | ||
jasmine.stringContaining('[percy:cli:update] Error: 503') | ||
]); | ||
}); | ||
}); |