diff --git a/.pnp.cjs b/.pnp.cjs index 39051ae..662c24d 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -34,6 +34,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./",\ "packageDependencies": [\ ["@actions/core", "npm:1.9.1"],\ + ["@actions/exec", "npm:1.1.1"],\ ["@actions/github", "npm:5.0.3"],\ ["@jest/globals", "npm:28.1.3"],\ ["@manypkg/get-packages", "npm:2.2.1"],\ @@ -62,6 +63,16 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@actions/exec", [\ + ["npm:1.1.1", {\ + "packageLocation": "./.yarn/cache/@actions-exec-npm-1.1.1-90973d2f96-d976e66dd5.zip/node_modules/@actions/exec/",\ + "packageDependencies": [\ + ["@actions/exec", "npm:1.1.1"],\ + ["@actions/io", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@actions/github", [\ ["npm:5.0.3", {\ "packageLocation": "./.yarn/cache/@actions-github-npm-5.0.3-057d4f5b0e-1d8e8c5c35.zip/node_modules/@actions/github/",\ @@ -85,6 +96,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@actions/io", [\ + ["npm:1.1.3", {\ + "packageLocation": "./.yarn/cache/@actions-io-npm-1.1.3-82d1cf012b-42841ac2b8.zip/node_modules/@actions/io/",\ + "packageDependencies": [\ + ["@actions/io", "npm:1.1.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@ampproject/remapping", [\ ["npm:2.2.0", {\ "packageLocation": "./.yarn/cache/@ampproject-remapping-npm-2.2.0-114878fa50-d74d170d06.zip/node_modules/@ampproject/remapping/",\ @@ -4776,6 +4796,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["root-workspace-0b6124", "workspace:."],\ ["@actions/core", "npm:1.9.1"],\ + ["@actions/exec", "npm:1.1.1"],\ ["@actions/github", "npm:5.0.3"],\ ["@jest/globals", "npm:28.1.3"],\ ["@manypkg/get-packages", "npm:2.2.1"],\ diff --git a/.yarn/cache/@actions-exec-npm-1.1.1-90973d2f96-d976e66dd5.zip b/.yarn/cache/@actions-exec-npm-1.1.1-90973d2f96-d976e66dd5.zip new file mode 100644 index 0000000..623edbd Binary files /dev/null and b/.yarn/cache/@actions-exec-npm-1.1.1-90973d2f96-d976e66dd5.zip differ diff --git a/.yarn/cache/@actions-io-npm-1.1.3-82d1cf012b-42841ac2b8.zip b/.yarn/cache/@actions-io-npm-1.1.3-82d1cf012b-42841ac2b8.zip new file mode 100644 index 0000000..835f7b5 Binary files /dev/null and b/.yarn/cache/@actions-io-npm-1.1.3-82d1cf012b-42841ac2b8.zip differ diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index bd12c81..5b83775 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/.yarn/sdks/typescript/bin/tsc b/.yarn/sdks/typescript/bin/tsc index 5608e57..454b950 100755 --- a/.yarn/sdks/typescript/bin/tsc +++ b/.yarn/sdks/typescript/bin/tsc @@ -1,13 +1,13 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire, createRequireFromPath} = require(`module`); +const {createRequire} = require(`module`); const {resolve} = require(`path`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); +const absRequire = createRequire(absPnpApiPath); if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { diff --git a/.yarn/sdks/typescript/bin/tsserver b/.yarn/sdks/typescript/bin/tsserver index cd7d557..d7a6056 100755 --- a/.yarn/sdks/typescript/bin/tsserver +++ b/.yarn/sdks/typescript/bin/tsserver @@ -1,13 +1,13 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire, createRequireFromPath} = require(`module`); +const {createRequire} = require(`module`); const {resolve} = require(`path`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); +const absRequire = createRequire(absPnpApiPath); if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { diff --git a/.yarn/sdks/typescript/lib/tsc.js b/.yarn/sdks/typescript/lib/tsc.js index 16042d0..2f62fc9 100644 --- a/.yarn/sdks/typescript/lib/tsc.js +++ b/.yarn/sdks/typescript/lib/tsc.js @@ -1,13 +1,13 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire, createRequireFromPath} = require(`module`); +const {createRequire} = require(`module`); const {resolve} = require(`path`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); +const absRequire = createRequire(absPnpApiPath); if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { diff --git a/.yarn/sdks/typescript/lib/tsserver.js b/.yarn/sdks/typescript/lib/tsserver.js index 9f9f4d6..bbb1e46 100644 --- a/.yarn/sdks/typescript/lib/tsserver.js +++ b/.yarn/sdks/typescript/lib/tsserver.js @@ -1,13 +1,13 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire, createRequireFromPath} = require(`module`); +const {createRequire} = require(`module`); const {resolve} = require(`path`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); +const absRequire = createRequire(absPnpApiPath); const moduleWrapper = tsserver => { if (!process.versions.pnp) { @@ -109,6 +109,8 @@ const moduleWrapper = tsserver => { str = `zip:${str}`; } break; } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); } } diff --git a/.yarn/sdks/typescript/lib/tsserverlibrary.js b/.yarn/sdks/typescript/lib/tsserverlibrary.js index 878b119..a68f028 100644 --- a/.yarn/sdks/typescript/lib/tsserverlibrary.js +++ b/.yarn/sdks/typescript/lib/tsserverlibrary.js @@ -1,13 +1,13 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire, createRequireFromPath} = require(`module`); +const {createRequire} = require(`module`); const {resolve} = require(`path`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); +const absRequire = createRequire(absPnpApiPath); const moduleWrapper = tsserver => { if (!process.versions.pnp) { @@ -109,6 +109,8 @@ const moduleWrapper = tsserver => { str = `zip:${str}`; } break; } + } else { + str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`); } } diff --git a/.yarn/sdks/typescript/lib/typescript.js b/.yarn/sdks/typescript/lib/typescript.js index cbdbf15..b5f4db2 100644 --- a/.yarn/sdks/typescript/lib/typescript.js +++ b/.yarn/sdks/typescript/lib/typescript.js @@ -1,20 +1,20 @@ #!/usr/bin/env node const {existsSync} = require(`fs`); -const {createRequire, createRequireFromPath} = require(`module`); +const {createRequire} = require(`module`); const {resolve} = require(`path`); const relPnpApiPath = "../../../../.pnp.cjs"; const absPnpApiPath = resolve(__dirname, relPnpApiPath); -const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); +const absRequire = createRequire(absPnpApiPath); if (existsSync(absPnpApiPath)) { if (!process.versions.pnp) { - // Setup the environment to be able to require typescript/lib/typescript.js + // Setup the environment to be able to require typescript require(absPnpApiPath).setup(); } } -// Defer to the real typescript/lib/typescript.js your application uses -module.exports = absRequire(`typescript/lib/typescript.js`); +// Defer to the real typescript your application uses +module.exports = absRequire(`typescript`); diff --git a/.yarn/sdks/typescript/package.json b/.yarn/sdks/typescript/package.json index b117d6a..4a0495f 100644 --- a/.yarn/sdks/typescript/package.json +++ b/.yarn/sdks/typescript/package.json @@ -2,5 +2,9 @@ "name": "typescript", "version": "4.7.4-sdk", "main": "./lib/typescript.js", - "type": "commonjs" + "type": "commonjs", + "bin": { + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" + } } diff --git a/package.json b/package.json index a47777d..e8cf0fd 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@actions/core": "^1.9.0", + "@actions/exec": "^1.1.1", "@actions/github": "^5.0.3", "@manypkg/get-packages": "^2.2.1", "@octokit/auth-app": "^3.6.1", diff --git a/renovate-changesets/action.yaml b/renovate-changesets/action.yaml new file mode 100644 index 0000000..cb76de7 --- /dev/null +++ b/renovate-changesets/action.yaml @@ -0,0 +1,11 @@ +name: Backstage Renovate Changeset Creator +description: Create changesets on the renovate bot PR's if needed +inputs: + multiple-workspaces: + description: If it's this repository is a collection of workspaces + required: false + +outputs: {} +runs: + using: node16 + main: ./entry.js diff --git a/renovate-changesets/entry.js b/renovate-changesets/entry.js new file mode 100644 index 0000000..d5cbadd --- /dev/null +++ b/renovate-changesets/entry.js @@ -0,0 +1,8 @@ +require('../.pnp.cjs').setup(); + +require('ts-node').register({ + transpileOnly: true, + project: require('path').resolve(__dirname, '../tsconfig.json'), +}); + +require('./index'); diff --git a/renovate-changesets/index.ts b/renovate-changesets/index.ts new file mode 100644 index 0000000..4cba368 --- /dev/null +++ b/renovate-changesets/index.ts @@ -0,0 +1,145 @@ +import * as core from '@actions/core'; +import { + commitAndPush, + createChangeset, + getBranchName, + getBumps, + getChangedFiles, + getChangesetFilename, + listPackages, +} from './renovateChangesets'; +import { relative as relativePath, resolve as resolvePath } from 'path'; + +async function main() { + core.info('Running Renovate Changesets'); + + const isMultipleWorkspaces = core.getBooleanInput('multiple-workspaces', { + required: false, + }); + + const branchName = await getBranchName(); + + if (!branchName.startsWith('renovate/')) { + core.info('Not a renovate branch, skipping'); + return; + } + + const allPackages = await listPackages({ + isMultipleWorkspaces, + includeRoots: true, + }); + + // Need to remove the topmost package if we're in a multi-workspace setup + const packageList = isMultipleWorkspaces + ? allPackages.filter(p => p.dir !== process.cwd()) + : allPackages; + + const changedFiles = await getChangedFiles(); + + // Group file changes by workspace, and drop workspaces without changes + const changedFilesByWorkspace = new Map( + packageList + .filter(p => p.isRoot) + .map(p => [ + p.dir, + changedFiles + .filter(f => f.startsWith(p.relativeDir)) + .map(f => relativePath(p.dir, f)), + ]) + .filter((workspaceChanges): workspaceChanges is [string, string[]] => { + const [_, files] = workspaceChanges; + return files.length > 0; + }), + ); + + // Check if those workspaces have changesets + const changedWorkspacesWithChangeset = new Map( + Array.from(changedFilesByWorkspace.entries()).map(([workspace, files]) => [ + workspace, + files.some(f => f.startsWith('.changeset/')), + ]), + ); + + // If all packages have a changeset already then exit early. + if ( + !changedWorkspacesWithChangeset.size || + Array.from(changedWorkspacesWithChangeset.values()).every(v => v) + ) { + core.info( + 'No changesets to create, or all workspaces have changesets already', + ); + return; + } + + // Get all package.jsons that were changed + const changedPackageJsons = new Map< + string, + { + path: string; + localPath: string; + packageJson: { name: string; version: string }; + }[] + >( + Array.from(changedFilesByWorkspace.entries()) + .map(([workspace, files]) => [ + workspace, + files.filter(f => f.endsWith('package.json')), + ]) + .filter((workspaceChanges): workspaceChanges is [string, string[]] => { + const [_, files] = workspaceChanges; + return files.length > 0; + }) + .map(([workspace, files]) => [ + workspace, + files.map(f => ({ + path: f, + localPath: relativePath(process.cwd(), resolvePath(workspace, f)), + packageJson: require(resolvePath(workspace, f)), + })), + ]), + ); + + if (!changedPackageJsons.size) { + core.info('Seems that no package.jsons were changed in this PR'); + return; + } + + // Get the bumps that happened in the last commit made by rennovate in the diff + const bumps = await Promise.all( + Array.from(changedPackageJsons.entries()).map( + async ([workspace, packages]) => { + const changes = await getBumps(packages.map(p => p.localPath)); + + return { + workspace, + packages, + changes, + }; + }, + ), + ); + + const changesetFilename = await getChangesetFilename(); + const changesetFiles: string[] = []; + + // Create a changeset for each of the workspaces in the right place + for (const bump of bumps) { + const changesetFilePath = resolvePath(bump.workspace, changesetFilename); + changesetFiles.push(changesetFilePath); + + await createChangeset( + changesetFilePath, + bump.changes, + bump.packages.map(p => p.packageJson.name), + ); + } + + // Commit and push all the changesets. + await commitAndPush(changesetFiles); +} + +main().catch(error => { + core.error(error.stack); + core.setFailed(String(error)); + process.exit(1); +}); diff --git a/renovate-changesets/renovateChangesets.ts b/renovate-changesets/renovateChangesets.ts new file mode 100644 index 0000000..d37c280 --- /dev/null +++ b/renovate-changesets/renovateChangesets.ts @@ -0,0 +1,105 @@ +import { getExecOutput, exec } from '@actions/exec'; +import fs from 'fs/promises'; +import { resolve as resolvePath, relative as relativePath } from 'path'; +import { getPackages, type Package } from '@manypkg/get-packages'; + +export async function getBranchName() { + const { stdout } = await getExecOutput('git', ['branch', '--show-current']); + return stdout; +} + +const findPackagesInDir = async ({ + dir, + includeRoots, +}: { + includeRoots: boolean; + dir: string; +}) => { + const { packages, rootPackage } = await getPackages(dir).catch(() => ({ + packages: [], + rootPackage: undefined, + })); + + return [...packages, rootPackage && { ...rootPackage, isRoot: true }] + .filter((p): p is Package & { isRoot?: boolean } => Boolean(p)) + .map(p => ({ + ...p, + isRoot: p.isRoot ?? false, + relativeDir: relativePath(process.cwd(), resolvePath(dir, p.relativeDir)), + })) + .filter(({ isRoot }) => (!includeRoots ? !isRoot : true)); +}; + +export async function getChangesetFilename() { + const { stdout: shortHash } = await getExecOutput( + 'git rev-parse --short HEAD', + ); + return `.changeset/renovate-${shortHash.trim()}.md`; +} + +export async function createChangeset( + fileName: string, + packageBumps: Map, + packages: string[], +) { + let message = ''; + for (const [pkg, bump] of packageBumps) { + message = message + `Updated dependency \`${pkg}\` to \`${bump}\`.\n`; + } + + const pkgs = packages.map(pkg => `'${pkg}': patch`).join('\n'); + const body = `---\n${pkgs}\n---\n\n${message.trim()}\n`; + await fs.writeFile(fileName, body); +} + +export const getChangedFiles = async () => { + const diffOutput = await getExecOutput('git diff --name-only HEAD~1'); + return diffOutput.stdout.split('\n'); +}; + +export async function getBumps(files: string[]) { + const bumps = new Map(); + for (const file of files) { + const { stdout: changes } = await getExecOutput('git', ['show', file]); + for (const change of changes.split('\n')) { + if (!change.startsWith('+ ')) { + continue; + } + const match = change.match(/"(.*?)"/g); + if (match) { + bumps.set(match[0].replace(/"/g, ''), match[1].replace(/"/g, '')); + } + } + } + return bumps; +} + +export async function commitAndPush(fileNames: string[]) { + await exec('git', ['add', ...fileNames]); + await exec('git commit -C HEAD --amend --no-edit'); + await exec('git push --force'); +} + +export async function listPackages({ + isMultipleWorkspaces, + includeRoots = false, +}: { + isMultipleWorkspaces?: boolean; + includeRoots?: boolean; +}): Promise<(Package & { isRoot: boolean })[]> { + if (!isMultipleWorkspaces) { + return findPackagesInDir({ dir: process.cwd(), includeRoots }); + } + + const workspacesRoot = resolvePath(process.cwd(), 'workspaces'); + const workspaceDirs = await fs.readdir(workspacesRoot); + + return await Promise.all( + workspaceDirs.map(workspace => + findPackagesInDir({ + dir: resolvePath(workspacesRoot, workspace), + includeRoots, + }), + ), + ).then(packages => packages.flat()); +} diff --git a/yarn.lock b/yarn.lock index 3ac27b3..b768af1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,6 +15,15 @@ __metadata: languageName: node linkType: hard +"@actions/exec@npm:^1.1.1": + version: 1.1.1 + resolution: "@actions/exec@npm:1.1.1" + dependencies: + "@actions/io": ^1.0.1 + checksum: d976e66dd51ab03d76a143da8e1406daa1bcdee06046168e6e0bec681c87a12999eefaad7a81cb81f28e4190610f55a58b8458ae4b82cbaaba13200490f4e8c2 + languageName: node + linkType: hard + "@actions/github@npm:^5.0.3": version: 5.0.3 resolution: "@actions/github@npm:5.0.3" @@ -36,6 +45,13 @@ __metadata: languageName: node linkType: hard +"@actions/io@npm:^1.0.1": + version: 1.1.3 + resolution: "@actions/io@npm:1.1.3" + checksum: 42841ac2b8a7afb29456b9edb5534dbe00148893c794bdbc17d29166847c51c884e2a7c087a489a428250a78e7b54bc761ba3b55eb2f97d9600e9193b60caf0b + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -3770,6 +3786,7 @@ __metadata: resolution: "root-workspace-0b6124@workspace:." dependencies: "@actions/core": ^1.9.0 + "@actions/exec": ^1.1.1 "@actions/github": ^5.0.3 "@jest/globals": ^28.1.1 "@manypkg/get-packages": ^2.2.1