-
Notifications
You must be signed in to change notification settings - Fork 153
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add update-backstage workflow (#2458)
Signed-off-by: Paul Schultz <[email protected]>
- Loading branch information
1 parent
5a37bcb
commit 72958b5
Showing
6 changed files
with
362 additions
and
4 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--- | ||
--- |
This file was deleted.
Oops, something went wrong.
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,7 @@ | ||
{ | ||
"root": true, | ||
"parserOptions": { | ||
"ecmaVersion": "latest", | ||
"sourceType": "module" | ||
} | ||
} |
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 @@ | ||
# Copyright 2024 The Janus IDP 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. | ||
|
||
name: Update Backstage | ||
|
||
env: | ||
TURBO_SCM_BASE: ${{ github.sha }} | ||
|
||
# enforce only one release action per release branch at a time | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: false | ||
|
||
on: | ||
workflow_dispatch: | ||
inputs: | ||
release: | ||
description: 'Backstage release version (e.g., 1.2.3)' | ||
required: false | ||
pattern: | ||
description: | | ||
Specify a glob pattern to select packages for upgrade (e.g., `@{backstage,backstage-community}/*`). | ||
required: false | ||
schedule: | ||
- cron: '15 3 * * WED' # Every Wednesday at 3:15 AM | ||
|
||
jobs: | ||
create-pr: | ||
name: Create PR | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- name: Generate token | ||
id: generate-token | ||
uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3 | ||
with: | ||
app-id: ${{ vars.JANUS_IDP_GITHUB_APP_ID }} | ||
private-key: ${{ secrets.JANUS_IDP_GITHUB_APP_PRIVATE_KEY }} | ||
|
||
- name: Checkout | ||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 | ||
with: | ||
token: ${{ steps.generate-token.outputs.token }} | ||
|
||
- name: Setup Node.js | ||
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4 | ||
with: | ||
node-version-file: '.nvmrc' | ||
registry-url: 'https://registry.npmjs.org' | ||
|
||
- name: Install dependencies | ||
run: yarn install | ||
|
||
- name: Run versons:bump script | ||
run: | | ||
yarn versions:bump \ | ||
${{ inputs.release && format('--release {0}', inputs.release) }} \ | ||
${{ inputs.pattern && format('--pattern {0}', inputs.pattern) }} | ||
- name: Determine PR details | ||
id: pr-details | ||
run: | | ||
if [[ -n "${{ inputs.release }}" ]]; then | ||
echo "commit_message=feat: update Backstage to ${{ inputs.release }}" >> $GITHUB_OUTPUT | ||
echo "branch=dependencies/backstage-${{ inputs.release }}" >> $GITHUB_OUTPUT | ||
elif [[ -n "${{ inputs.pattern }}" ]]; then | ||
sanitized_branch=$(echo "${{ inputs.pattern }}" | tr -d '@{}*' | tr '/' '-' | tr ',' '.') | ||
# Remove trailing hyphen if present | ||
sanitized_branch=${sanitized_branch%-*} | ||
echo "commit_message=feat: update Backstage plugins dependencies with pattern ${{ inputs.pattern }}" >> $GITHUB_OUTPUT | ||
echo "branch=dependencies/${sanitized_branch}" >> $GITHUB_OUTPUT | ||
else | ||
echo "commit_message=feat: update Backstage to the latest version" >> $GITHUB_OUTPUT | ||
echo "branch=dependencies/backstage-latest" >> $GITHUB_OUTPUT | ||
fi | ||
- name: Create Pull Request | ||
uses: peter-evans/create-pull-request@v7 | ||
with: | ||
token: ${{ steps.generate-token.outputs.token }} | ||
commit-message: ${{ steps.pr-details.outputs.commit_message }} | ||
title: ${{ steps.pr-details.outputs.commit_message }} | ||
branch: ${{ steps.pr-details.outputs.branch }} | ||
base: main | ||
signoff: true | ||
sign-commits: true |
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,255 @@ | ||
import glob from 'glob' | ||
import { execSync } from 'node:child_process' | ||
import { readFileSync, writeFileSync } from 'node:fs' | ||
import { dirname, join } from 'node:path' | ||
import { fileURLToPath } from 'node:url' | ||
import semver from 'semver' | ||
|
||
const __filename = fileURLToPath(import.meta.url) | ||
const __dirname = dirname(__filename) | ||
|
||
const ROOT_DIR = join(__dirname, '..') | ||
const PLUGINS_DIR = join(ROOT_DIR, 'plugins') | ||
const BACKSTAGE_JSON_PATH = join(ROOT_DIR, 'backstage.json') | ||
const PACKAGE_JSON_GLOB = '**/package.json' | ||
const IGNORE_GLOB = ['**/node_modules/**'] | ||
const BACKSTAGE_RELEASES_API = 'https://api.github.com/repos/backstage/backstage/releases' | ||
|
||
// Change directory to the root of the project | ||
process.chdir(ROOT_DIR) | ||
|
||
/** | ||
* Pins dependencies in package.json files by removing the caret (^) from version ranges. | ||
*/ | ||
function pinDependencies() { | ||
const packageJsonFiles = glob.sync(PACKAGE_JSON_GLOB, { | ||
ignore: IGNORE_GLOB | ||
}) | ||
|
||
for (const packageJsonFile of packageJsonFiles) { | ||
try { | ||
const packageJsonPath = join(process.cwd(), packageJsonFile) | ||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) | ||
// Replace all instances of "^" with "" in package.json dependencies | ||
for (const depType of ['devDependencies']) { | ||
if (packageJson[depType]) { | ||
for (const depName in packageJson[depType]) { | ||
packageJson[depType][depName] = packageJson[depType][depName].replace(/^\^/, '') | ||
} | ||
} | ||
} | ||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8') | ||
} catch (error) { | ||
console.error(`Error processing ${packageJsonFile}:`, error) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Fetches the latest Backstage version from the GitHub API. | ||
* | ||
* @returns {Promise<string>} The latest Backstage version. | ||
*/ | ||
async function getLatestBackstageVersion() { | ||
try { | ||
const res = await fetch(BACKSTAGE_RELEASES_API) | ||
const data = await res.json() | ||
const versions = data | ||
.map((release) => release.tag_name) | ||
.filter((version) => semver.valid(version) && !semver.prerelease(version)) | ||
return semver.maxSatisfying(versions, '*').substring(1) | ||
} catch (error) { | ||
console.error('Error fetching latest Backstage version:', error) | ||
throw error | ||
} | ||
} | ||
|
||
/** | ||
* Updates the Backstage version in the backstage.json file. | ||
* | ||
* @param {string} version - The new Backstage version. | ||
*/ | ||
function updateBackstageVersionFile(version) { | ||
try { | ||
const data = { version } | ||
writeFileSync(BACKSTAGE_JSON_PATH, JSON.stringify(data, null, 2) + '\n', 'utf8') | ||
} catch (error) { | ||
console.error('Error updating Backstage version file:', error) | ||
} | ||
} | ||
|
||
/** | ||
* Updates the `backstage.supported-versions` field in package.json files under the `PLUGINS_DIR`. | ||
* | ||
* @param {string} backstageVersion - The Backstage version to set. | ||
*/ | ||
function updateSupportedBackstageVersions(backstageVersion) { | ||
const packageJsonFiles = glob.sync(PACKAGE_JSON_GLOB, { | ||
cwd: PLUGINS_DIR, // Search only within PLUGINS_DIR | ||
ignore: IGNORE_GLOB | ||
}) | ||
|
||
for (const packageJsonFile of packageJsonFiles) { | ||
try { | ||
const packageJsonPath = join(PLUGINS_DIR, packageJsonFile) | ||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) | ||
|
||
// Update backstage.supported-versions | ||
packageJson['backstage'] = { | ||
...packageJson['backstage'], | ||
'supported-versions': backstageVersion | ||
} | ||
|
||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8') | ||
} catch (error) { | ||
console.error(`Error processing ${packageJsonFile}:`, error) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Parses command line arguments and returns an object with flag values. | ||
* | ||
* @returns {{ hasReleaseFlag: boolean, hasPatternFlag: boolean, releaseVersion: string, pattern: string }} | ||
*/ | ||
function parseArguments() { | ||
const args = process.argv.slice(2) | ||
const releaseIndex = args.indexOf('--release') | ||
const patternIndex = args.indexOf('--pattern') | ||
const hasReleaseFlag = releaseIndex !== -1 | ||
const hasPatternFlag = patternIndex !== -1 | ||
|
||
// Ensure that --pattern and --release are not used together | ||
if (hasReleaseFlag && hasPatternFlag) { | ||
console.error('Error: The --pattern and --release flags cannot be used together.') | ||
process.exit(1) | ||
} | ||
|
||
let releaseVersion = '' | ||
let pattern = '' | ||
if (hasReleaseFlag) { | ||
releaseVersion = args[releaseIndex + 1] | ||
if (!releaseVersion) { | ||
console.error( | ||
`Error: The '--release' flag requires a version argument to bump to a specific Backstage release line or version (default: "main").` | ||
) | ||
process.exit(1) | ||
} | ||
} else if (hasPatternFlag) { | ||
pattern = args[patternIndex + 1] | ||
if (!pattern) { | ||
console.error( | ||
"Error: The '--pattern' flag requires a glob pattern to specify which packages to upgrade." | ||
) | ||
process.exit(1) | ||
} | ||
} | ||
|
||
return { | ||
hasReleaseFlag, | ||
hasPatternFlag, | ||
releaseVersion, | ||
pattern | ||
} | ||
} | ||
|
||
/** | ||
* Constructs the bump command based on the provided flags. | ||
* | ||
* @param {boolean} hasReleaseFlag | ||
* @param {boolean} hasPatternFlag | ||
* @param {string} releaseVersion | ||
* @param {string} pattern | ||
* @returns {string} | ||
*/ | ||
function constructBumpCommand(hasReleaseFlag, hasPatternFlag, releaseVersion, pattern) { | ||
let bumpCommand = 'backstage-cli versions:bump --skip-install' | ||
if (hasReleaseFlag) { | ||
bumpCommand += ` --release ${releaseVersion}` | ||
} else if (hasPatternFlag) { | ||
bumpCommand += ` --pattern ${pattern}` | ||
} | ||
return bumpCommand | ||
} | ||
|
||
/** | ||
* Determines the Backstage version to use based on flags and pattern. | ||
* | ||
* @param {boolean} hasReleaseFlag | ||
* @param {boolean} hasPatternFlag | ||
* @param {string} releaseVersion | ||
* @param {string} pattern | ||
* @returns {Promise<string>} | ||
*/ | ||
async function determineBackstageVersion(hasReleaseFlag, hasPatternFlag, releaseVersion, pattern) { | ||
if (hasReleaseFlag) { | ||
return releaseVersion | ||
} | ||
|
||
// implies that we are updating to the latest backstage version | ||
if (!hasPatternFlag) { | ||
return await getLatestBackstageVersion() | ||
} | ||
|
||
// implies that we are updating to the latest backstage version because `backstage` is include in the pattern | ||
if (hasPatternFlag && /backstage[^-]/.test(pattern)) { | ||
return await getLatestBackstageVersion() | ||
} | ||
|
||
// fetch the version from `backstage.json` | ||
try { | ||
const backstageJson = JSON.parse(readFileSync(BACKSTAGE_JSON_PATH, 'utf8')) | ||
return backstageJson.version | ||
} catch (error) { | ||
console.error('Error reading Backstage version:', error) | ||
process.exit(1) | ||
} | ||
} | ||
|
||
/** | ||
* The main function that orchestrates the update process. | ||
*/ | ||
async function main() { | ||
try { | ||
const { hasReleaseFlag, hasPatternFlag, releaseVersion, pattern } = parseArguments() | ||
|
||
const bumpCommand = constructBumpCommand( | ||
hasReleaseFlag, | ||
hasPatternFlag, | ||
releaseVersion, | ||
pattern | ||
) | ||
|
||
console.log('Bumping version...') | ||
execSync(bumpCommand, { stdio: 'inherit' }) | ||
|
||
console.log('Pinning all dependencies...') | ||
pinDependencies() | ||
|
||
const backstageVersion = await determineBackstageVersion( | ||
hasReleaseFlag, | ||
hasPatternFlag, | ||
releaseVersion, | ||
pattern | ||
) | ||
|
||
console.log(`Updating plugins supported versions to ${backstageVersion}...`) | ||
updateSupportedBackstageVersions(backstageVersion) | ||
|
||
console.log('Updating lockfile...') | ||
execSync('yarn install --no-immutable', { stdio: 'inherit' }) | ||
|
||
console.log('Deduping lockfile...') | ||
execSync('yarn dedupe', { stdio: 'inherit' }) | ||
|
||
console.log(`Updating backstage.json to ${backstageVersion}...`) | ||
updateBackstageVersionFile(backstageVersion) | ||
|
||
console.log(`Successfully updated the Backstage Showcase to ${backstageVersion}!`) | ||
} catch (error) { | ||
console.error('An error occurred during the update process:', error) | ||
process.exit(1) | ||
} | ||
} | ||
|
||
await main() |