diff --git a/.changeset/patched.json b/.changeset/patched.json new file mode 100644 index 0000000000..1029c8f106 --- /dev/null +++ b/.changeset/patched.json @@ -0,0 +1,3 @@ +{ + "currentReleaseVersion": {} +} diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index a649904a01..e960d45744 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -13,6 +13,8 @@ jobs: - uses: actions/checkout@v2 - name: Install Dependencies run: yarn --frozen-lockfile + - name: Run pre-release preparations + run: node scripts/pre-release.js - name: Create Release Pull Request uses: changesets/action@master with: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 8106dd1198..2a6e3dc6f9 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -3,7 +3,7 @@ name: Main Master Build on: workflow_dispatch: push: - branches: [master] + branches: [master, release-*-patch] jobs: build: diff --git a/SECURITY.md b/SECURITY.md index ba96c694ee..c825cdd97e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -36,3 +36,20 @@ There are many situations where a vulnerability does not affect a particular dep To work around this and other similar issues, Snyk provides a method to ignore vulnerabilities. In order to provide the best visibility and most utility to adopters of Backstage, we store these ignore rules in `.snyk` policy files. This allows adopters to rely on our ignore policies if they wish to do so. Adding a new ignore policy is done by creating or modifying an existing `.snyk` file within a package root. See the [Snyk Documentation](https://support.snyk.io/hc/en-us/articles/360007487097-The-snyk-file) for details on the syntax. Always include a description, full path, and time limit of the ignore policy. + +TODO: MOVE TO SEPARATE FILE + +## Emergency Release Process + +This emergency release process is intended only for the Backstage maintainers. + +For this example we will be using the `@backstage/core` package which is currently version `1.5.0` in the master branch. + +In the event of a severe bug or security vulnerability detected in `1.1.1` which was released in `2048-01-01` please proceed using this process. + +- [ ] Create new base branch using `git checkout 2048-01-01 && git checkout -b release-2048-01-01-patch && git push --set-upstream origin release-2048-01-01-patch` +- [ ] Create new branch of the base branch(`release-2048-01-01-patch`) that will contain the emergency fix. `git checkout -b release-2048-01-01-emergency-fix` +- [ ] Apply fixes and generate new patch changeset for the effected package. +- [ ] Create PR towards the base branch(`release-2048-01-01-patch`) with the emergency fix. +- [ ] Review/Merge PR towards `release-2048-01-01-patch`. This will automatically trigger a release. +- [ ] Apply security fix towards master. diff --git a/scripts/pre-release.js b/scripts/pre-release.js new file mode 100644 index 0000000000..c9b004ba60 --- /dev/null +++ b/scripts/pre-release.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/* eslint-disable import/no-extraneous-dependencies */ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs-extra'); +const semver = require('semver'); +const { getPackages } = require('@manypkg/get-packages'); +const path = require('path'); + +const DEPENDENCY_TYPES = [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', +]; + +/* + +Shipping to main TODO: + +- [] Document emergency release flow +- [x] Workflow that triggers releases of `release-*-patch` branches +- [x] Branch protection of `release-*-patch` branches +- [x] Bring release.js + patched.json into the main repo +- [] profit + + +*/ + +async function main() { + const patchedJsonPath = path.resolve('.changeset', 'patched.json'); + const { currentReleaseVersion } = await fs.readJSON(patchedJsonPath); + if (Object.keys(currentReleaseVersion).length === 0) { + console.log('No currentReleaseVersion overrides found, skipping.'); + return; + } + + const { packages } = await getPackages(path.resolve('.')); + console.log('packages', packages); + + const pendingVersionBumps = new Map(); + + for (const [name, version] of Object.entries(currentReleaseVersion)) { + const pkg = packages.find(p => p.packageJson.name === name); + if (!pkg) { + throw new Error(`Package ${name} not found`); + } + + if (!semver.valid(version)) { + throw new Error(`Invalid base version ${version} for package ${name}`); + } + + const currentVersion = semver.parse(pkg.packageJson.version); + const targetVersion = semver.parse(version).inc('patch'); + targetVersion.prerelease = currentVersion.prerelease; + + const targetVersionString = targetVersion.format(); + pendingVersionBumps.set(name, { + targetVersion: targetVersionString, + targetRange: `^${targetVersionString}`, + }); + } + + console.log('DEBUG: pendingVersionBumps =', pendingVersionBumps); + + for (const { dir, packageJson } of packages) { + let hasChanges = false; + + if (pendingVersionBumps.has(packageJson.name)) { + packageJson.version = pendingVersionBumps.get( + packageJson.name, + ).targetVersion; + hasChanges = true; + } + + for (const depType of DEPENDENCY_TYPES) { + const deps = packageJson[depType]; + for (const depName of Object.keys(deps ?? {})) { + const currentRange = deps[depName]; + + if (pendingVersionBumps.has(depName)) { + const pendingBump = pendingVersionBumps.get(depName); + console.log( + `DEBUG: replacing ${depName} ${currentRange} with ${pendingBump.targetRange} in ${depType} of ${packageJson.name}`, + ); + deps[depName] = pendingBump.targetRange; + hasChanges = true; + } + } + } + + if (hasChanges) { + console.log('writing package', packageJson.name); + await fs.writeJSON(path.resolve(dir, 'package.json'), packageJson, { + spaces: 2, + }); + } + } + + await fs.writeJSON( + patchedJsonPath, + { currentReleaseVersion: {} }, + { spaces: 2 }, + ); +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +});