Skip to content

Commit

Permalink
chore: add update-backstage workflow (#2458)
Browse files Browse the repository at this point in the history
Signed-off-by: Paul Schultz <[email protected]>
  • Loading branch information
schultzp2020 authored Oct 29, 2024
1 parent 5a37bcb commit 72958b5
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .changeset/nasty-weeks-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
3 changes: 0 additions & 3 deletions .eslintrc.js

This file was deleted.

7 changes: 7 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"root": true,
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}
97 changes: 97 additions & 0 deletions .github/workflows/update-backstage.yaml
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"prettier:fix": "turbo run prettier:fix",
"new": "janus-cli new --do-not-edit-packages",
"prepare": "husky install",
"versions:bump": "backstage-cli versions:bump --skip-install && find . -name 'package.json' ! -path '*/node_modules/*' -exec sed -i -e '/devDependencies/,/\\\\}/{ s/\\\"\\\\^/\\\"/g; }' {} \\; && yarn install",
"versions:bump": "node ./scripts/update-backstage.mjs",
"packages:version": "changeset version",
"packages:publish": "turbo run build --concurrency=75% --filter='@janus-idp/*' && changeset publish"
},
Expand Down
255 changes: 255 additions & 0 deletions scripts/update-backstage.mjs
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()

0 comments on commit 72958b5

Please sign in to comment.