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/styles/vocab.txt b/.github/styles/vocab.txt index b24d5e77f2..5a47b720b8 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -16,6 +16,7 @@ Autoscaling autoselect Avro backrub +backported Bigtable Billett Bitbucket diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index a649904a01..44ddbe3334 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 release preparations + run: node scripts/prepare-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/docs/plugins/publishing.md b/docs/plugins/publishing.md deleted file mode 100644 index 06fae0533e..0000000000 --- a/docs/plugins/publishing.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -id: publishing -title: Publishing -description: Documentation on Publishing npm packages ---- - -## npm - -npm packages are published through CI/CD in the -[`.github/workflows/master.yml`](https://github.com/backstage/backstage/blob/master/.github/workflows/master.yml) -workflow. Every commit that is merged to master will be checked for new versions -of all public packages, and any new versions will automatically be published to -npm. - -### Creating a new release - -Version bumps are made through release PRs. To create a new release, checkout -out a new branch that you will use for the release, e.g. - -```sh -$ git checkout -b new-release -``` - -First bump the `CHANGELOG.md` in the root of the repo and commit. You bump it by -adding a header for the new version just below the `## Next Release` one. - -Then, from the root of the repo, run - -```sh -$ yarn release -``` - -This will bring up the lerna release CLI where you choose what type of version -bump you want to make, (major/minor/patch/prerelease). The CLI will take you -through choosing a version, previewing all changes, and then approving the -release. Once the release is approved, a new commit is created that you can -submit as a PR. Push the branch to GitHub: - -```sh -$ git push origin -u new-release -``` - -And then create a PR. Once the PR is approved and merged into master, the master -build will publish new versions of all bumped packages. - -### Include new changes in existing release PR - -If you want to include some last minute changes to an existing release PR, -follow these instructions: - -```sh -$ git checkout master -$ git pull -$ git checkout new-release -$ git reset --hard master -$ yarn release -$ git push --force -``` diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 0000000000..e5642a3467 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,72 @@ + + +## npm + +npm packages are published through CI/CD in the +[`.github/workflows/master.yml`](https://github.com/backstage/backstage/blob/master/.github/workflows/master.yml) +workflow. Every commit that is merged to master will be checked for new versions +of all public packages, and any new versions will automatically be published to +npm. + +### Creating a new release + +Releases are handled by changesets and trigger whenever the "Version Packages" +PR is merged. This is typically done every Thursday around noon CET. + +## Emergency Release Process + +**This emergency release process is intended only for the Backstage +maintainers.** + +For this example we will be using the `@backstage/plugin-foo` package as an +example and assume that it is currently version `1.5.0` in the master branch. + +In the event of a severe bug being introduced in version `1.5.0` of the +`@backstage/plugin-foo` released in the `2048-01-01` release, the following +process is used to release an emergency fix as `1.5.1`: + +- [ ] Identify the release that needs to be patched, in this case we're fixing a + broken release, so it would be the most recent one, `2048-01-01`. In the + event of a backported security fix, the release that has the last + published version of each major version of the package should be the one + patched. +- [ ] Make sure a patch branch exists for the release that is being patched. If + a patch already exists, reuse the existing branch. The branch **must + always** be named exactly `release--patch`. + + ```bash + git checkout release-2048-01-01 + git checkout -b release-2048-01-01-patch + git push --set-upstream origin release-2048-01-01-patch + ``` + +- [ ] With the `release-2048-01-01-patch` branch as a base, create a new branch + for your fix. This branch can be named anything, but the following naming + pattern may be suitable: + + ```bash + git checkout -b ${USER}/release-2048-01-01-emergency-fix + ``` + +- [ ] Apply fixes and create a new patch changeset for the affected package, + then commit these changes. +- [ ] Run `yarn release` in the root of the repo in order to convert your + changeset into package version bumps and changelog entries. Commit these + changes as a second `"Generated release"` commit. +- [ ] Create PR towards the base branch (`release-2048-01-01-patch`) containing + the two commits. +- [ ] Review/Merge the PR into `release-2048-01-01-patch`. This will + automatically trigger a release. +- [ ] Make sure the same fix is applied in master before the next release, and + create an appropriate changeset for that as well. In the changeset towards + master you should refer back to all patch releases that also received the + same fix. Also be sure to update `.changeset/patched.json` in the same PR + to make sure that future releases of the packages are bumped accordingly: + + ```json + { + "currentReleaseVersion": { + "@backstage/plugin-foo": "1.5.1" + } + } + ``` diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 6c4c5261d9..c0a0b9b24f 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -202,7 +202,6 @@ "type": "subcategory", "label": "Publishing", "ids": [ - "plugins/publishing", "plugins/publish-private", "plugins/add-to-marketplace", "plugins/observability" diff --git a/mkdocs.yml b/mkdocs.yml index 81dad55b84..398863a3c0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -126,7 +126,6 @@ nav: - Testing: - Testing with Jest: 'plugins/testing.md' - Publishing: - - Publishing: 'plugins/publishing.md' - Publish private: 'plugins/publish-private.md' - Add to Marketplace: 'plugins/add-to-marketplace.md' - Observability: 'plugins/observability.md' diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js new file mode 100755 index 0000000000..3d4f142a8d --- /dev/null +++ b/scripts/prepare-release.js @@ -0,0 +1,110 @@ +#!/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', +]; + +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('.')); + + 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}`, + }); + } + + 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 (currentRange === '*') { + continue; + } + + if (pendingVersionBumps.has(depName)) { + const pendingBump = pendingVersionBumps.get(depName); + console.log( + `Replacing ${depName} ${currentRange} with ${pendingBump.targetRange} in ${depType} of ${packageJson.name}`, + ); + deps[depName] = pendingBump.targetRange; + hasChanges = true; + } + } + } + + if (hasChanges) { + 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); +});