update_browser_revision.mjs (7114B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import {execSync, exec} from 'child_process'; 8 import {writeFile, readFile} from 'fs/promises'; 9 import {promisify} from 'util'; 10 11 import actions from '@actions/core'; 12 import {resolveBuildId} from '@puppeteer/browsers'; 13 import {SemVer} from 'semver'; 14 15 import packageJson from '../packages/puppeteer-core/package.json' with {type: 'json'}; 16 import versionData from '../versions.json' with {type: 'json'}; 17 18 import {PUPPETEER_REVISIONS} from 'puppeteer-core/internal/revisions.js'; 19 20 const execAsync = promisify(exec); 21 22 const BROWSER = process.env.BROWSER_TO_UPDATE; 23 24 if (!BROWSER) { 25 console.error('No BROWSER_TO_UPDATE env variable supplied!'); 26 process.exit(1); 27 } 28 29 const BROWSER_CURRENT_VERSION = PUPPETEER_REVISIONS[BROWSER]; 30 31 const touchedFiles = []; 32 33 function getCapitalize(text) { 34 return text.charAt(0).toUpperCase() + text.slice(1); 35 } 36 37 /** 38 * 39 * @param {string} version 40 * @returns {string} 41 */ 42 function normalizeVersionForCommit(browser, version) { 43 switch (browser) { 44 case 'firefox': 45 // Splits the prefix of `stable_` for Firefox 46 return version.split('_').at(-1); 47 case 'chrome': 48 return version; 49 } 50 51 throw new Error(`Unrecognized browser ${browser}`); 52 } 53 54 /** 55 * 56 * @param {string} version 57 * @returns {string} 58 */ 59 function normalizeVersionToSemVer(browser, version) { 60 switch (browser) { 61 case 'firefox': 62 // Splits the prefix of `stable_` for Firefox 63 version = version.split('_').at(-1); 64 // Firefox reports 129.0 instead of 129.0.0 65 // Patch have the correct number 128.0.2 66 if (version.split('.').length <= 2) { 67 return `${version}.0`; 68 } 69 70 return version; 71 case 'chrome': 72 // For Chrome (example: 127.0.6533.99) is allowed as SemVer 73 // as long as we use the loose option. 74 return version; 75 } 76 77 throw new Error(`Unrecognized browser ${browser}`); 78 } 79 80 function checkIfNeedsUpdate(browser, oldVersion, newVersion) { 81 const oldSemVer = new SemVer( 82 normalizeVersionToSemVer(browser, oldVersion), 83 true, 84 ); 85 const newSemVer = new SemVer( 86 normalizeVersionToSemVer(browser, newVersion), 87 true, 88 ); 89 let message = `roll to ${getCapitalize(browser)} ${normalizeVersionForCommit(browser, newVersion)}`; 90 91 if (newSemVer.compare(oldSemVer) <= 0) { 92 // Exit the process without setting up version 93 console.warn( 94 `Version ${newVersion} is older or the same as the current ${oldVersion}`, 95 ); 96 process.exit(0); 97 } else if (newSemVer.major === oldSemVer.major) { 98 message = `fix: ${message}`; 99 } else { 100 message = `feat: ${message}`; 101 } 102 actions.setOutput('commit', message); 103 } 104 105 /** 106 * We cant use `npm run format` as it's too slow 107 * so we only scope the files we updated 108 */ 109 async function formatUpdateFiles() { 110 await Promise.all( 111 touchedFiles.map(file => { 112 return execAsync(`npx eslint --fix ${file}`); 113 }), 114 ); 115 await Promise.all( 116 touchedFiles.map(file => { 117 return execAsync(`npx prettier --write ${file}`); 118 }), 119 ); 120 } 121 122 async function replaceInFile(filePath, search, replace) { 123 const buffer = await readFile(filePath); 124 const update = buffer.toString().replaceAll(search, replace); 125 126 await writeFile(filePath, update); 127 128 touchedFiles.push(filePath); 129 } 130 131 async function getVersionForStable(browser) { 132 return await resolveBuildId(browser, 'linux', 'stable'); 133 } 134 135 async function updateDevToolsProtocolVersion(browserVersion) { 136 const result = await fetch( 137 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json', 138 ).then(response => { 139 return response.json(); 140 }); 141 142 const {version, revision} = result.channels['Stable']; 143 if (browserVersion !== version) { 144 console.error( 145 'The version from CfT website and @puppeteer/browser mismatch.', 146 ); 147 process.exit(1); 148 } 149 150 const currentProtocol = packageJson.dependencies['devtools-protocol']; 151 const command = `npm view "devtools-protocol@<=0.0.${revision}" version | tail -1`; 152 153 const bestNewProtocol = execSync(command, { 154 encoding: 'utf8', 155 }) 156 .split(' ')[1] 157 .replace(/'|\n/g, ''); 158 159 await replaceInFile( 160 './packages/puppeteer-core/package.json', 161 `"devtools-protocol": "${currentProtocol}"`, 162 `"devtools-protocol": "${bestNewProtocol}"`, 163 ); 164 165 await replaceInFile( 166 './packages/puppeteer/package.json', 167 `"devtools-protocol": "${currentProtocol}"`, 168 `"devtools-protocol": "${bestNewProtocol}"`, 169 ); 170 } 171 172 async function saveVersionData() { 173 await writeFile('./versions.json', JSON.stringify(versionData, null, 2)); 174 touchedFiles.push('./versions.json'); 175 } 176 177 async function updateVersionData(browser, oldVersion, newVersion) { 178 const browserVersions = versionData.versions.map( 179 ([_puppeteerVersion, browserVersions]) => { 180 return browserVersions[browser]; 181 }, 182 ); 183 if (browserVersions.indexOf(newVersion) !== -1) { 184 // Already updated. 185 return; 186 } 187 188 const nextVersionConfig = versionData.versions.find(([puppeteerVersion]) => { 189 return puppeteerVersion === 'NEXT'; 190 }); 191 192 // If we have manually rolled Chrome but not yet released 193 // We will have NEXT as value in the Map 194 if (nextVersionConfig) { 195 nextVersionConfig[1][browser] = newVersion; 196 return; 197 } 198 199 versionData.versions.unshift([ 200 'NEXT', 201 { 202 ...versionData.versions.at(0).at(1), 203 [browser]: newVersion, 204 }, 205 ]); 206 } 207 208 async function updateLastMaintainedChromeVersion(oldVersion, newVersion) { 209 const browserVersions = versionData.versions.map( 210 ([_puppeteerVersion, browserVersions]) => { 211 return browserVersions['chrome']; 212 }, 213 ); 214 if (browserVersions.indexOf(newVersion) !== -1) { 215 // Already updated. 216 return; 217 } 218 219 const oldSemVer = new SemVer(oldVersion, true); 220 const newSemVer = new SemVer(newVersion, true); 221 222 if (newSemVer.compareMain(oldSemVer) !== 0) { 223 const lastMaintainedSemVer = new SemVer( 224 versionData.lastMaintainedChromeVersion, 225 true, 226 ); 227 const newLastMaintainedMajor = lastMaintainedSemVer.major + 1; 228 229 const nextMaintainedVersion = browserVersions.find(version => { 230 return new SemVer(version, true).major === newLastMaintainedMajor; 231 }); 232 233 versionData.lastMaintainedChromeVersion = nextMaintainedVersion; 234 } 235 } 236 237 const version = await getVersionForStable(BROWSER); 238 239 checkIfNeedsUpdate(BROWSER, BROWSER_CURRENT_VERSION, version); 240 241 await replaceInFile( 242 './packages/puppeteer-core/src/revisions.ts', 243 BROWSER_CURRENT_VERSION, 244 version, 245 ); 246 247 await updateVersionData(BROWSER, BROWSER_CURRENT_VERSION, version); 248 249 if (BROWSER === 'chrome') { 250 await updateLastMaintainedChromeVersion(BROWSER_CURRENT_VERSION, version); 251 await updateDevToolsProtocolVersion(version); 252 // Create new `package-lock.json` as we update devtools-protocol 253 execSync('npm install --ignore-scripts'); 254 } 255 256 await saveVersionData(); 257 258 // Make sure we pass CI formatter check by running all the new files though it 259 await formatUpdateFiles(); 260 261 // Keep this as they can be used to debug GitHub Actions if needed 262 actions.setOutput('version', version);