Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Workflow to update JupyterLab dependencies automatically #7281

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
92 changes: 92 additions & 0 deletions .github/workflows/upgrade-juypterlab-dependencies.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Check for latest JupyterLab releases

on:
schedule:
- cron: 30 17 * * *
workflow_dispatch:
inputs:
version:
description: 'JupyterLab version'
default: latest
required: true
type: string

env:
version_tag: 'latest'

permissions:
contents: write
pull-requests: write

jobs:
check_for_lab_updates:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install required dependencies
run: |
python -m pip install jupyterlab
sudo apt-get install hub

- name: Install Node
uses: actions/setup-node@v2
with:
node-version: '20.x'

- name: Install npm dependencies and build buildutils
run: |
jlpm install
jlpm run build:utils

- name: Check for new releases and update
shell: bash
run: |
set -eux
for version in ${{ inputs.version || env.version_tag }}
do
export LATEST=$(node buildutils/lib/get-latest-lab-version.js --set-version $version)
done

echo "latest=${LATEST}" >> $GITHUB_ENV
node buildutils/lib/upgrade-lab-dependencies.js --set-version ${LATEST}
if [[ ! -z "$(git status --porcelain package.json)" ]]; then
jlpm install
fi

- name: Create a PR
shell: bash
env:
GITHUB_USER: ${{ secrets.G_USER }}
GITHUB_TOKEN: ${{ secrets.G_TOKEN }}
run: |
set -eux

export LATEST=${{ env.latest }}
export BRANCH_NAME=update-to-v${LATEST}

# if resulted in any change:
if [[ ! -z "$(git status --porcelain package.json)" ]]; then
# if branch already exists.
if git ls-remote --heads origin | grep "refs/heads/${BRANCH_NAME}$" > /dev/null; then
echo "Branch '${BRANCH_NAME}' exists."
else
# new branch is created
git checkout -b "${BRANCH_NAME}"
git config user.name "Jupyter Bot"
git config user.email '[email protected]'

git commit . -m "Update to JupyterLab v${LATEST}"

git push --set-upstream origin "${BRANCH_NAME}"
hub pull-request -m "Update to JupyterLab v${LATEST}" \
-m "New JupyterLab release [v${LATEST}](https://github.com/jupyterlab/jupyterlab/releases/tag/v${LATEST}) is available. Please review the lock file carefully.".
fi
fi
54 changes: 54 additions & 0 deletions buildutils/src/get-latest-lab-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
function extractVersionFromReleases(
releases: any,
versionTag: string
): string | null {
for (const release of releases) {
const tagName: string = release['tag_name'];
if (versionTag === 'latest') {
if (!release['prerelease'] && !release['draft']) {
return tagName;
}
} else if (versionTag === tagName) {
return tagName;
}
}
return null;
}

async function findVersion(versionTag: string): Promise<string> {
const url = 'https://api.github.com/repos/jupyterlab/jupyterlab/releases';
const response = await fetch(url);
if (!response.ok) {
const error_message = `Failed to fetch package.json from ${url}. HTTP status code: ${response.status}`;
throw new Error(error_message);
}
const releases: any = await response.json();
const version: string | null = extractVersionFromReleases(
releases,
versionTag
);
if (version === null) {
const error_message = 'Invalid release tag';
throw new Error(error_message);
}
return version.substring(1);
}

async function getLatestLabVersion(): Promise<void> {
const args: string[] = process.argv.slice(2);
if (args.length !== 2 || args[0] !== '--set-version') {
console.error('Usage: node script.js --set-version <version>');
process.exit(1);
}
const version_tag: string = args[1];

try {
const result: string = await findVersion(version_tag);
console.log(result);
} catch (error: any) {
console.error('Error:', error.message);
process.exit(1);
}
}

getLatestLabVersion();
101 changes: 101 additions & 0 deletions buildutils/src/upgrade-lab-dependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import fs from 'fs';
import path from 'path';

const PACKAGE_JSON_PATHS: string[] = [
'app/package.json',
'buildutils/package.json',
'package.json',
'packages/application-extension/package.json',
'packages/application/package.json',
'packages/console-extension/package.json',
'packages/docmanager-extension/package.json',
'packages/documentsearch-extension/package.json',
'packages/help-extension/package.json',
'packages/lab-extension/package.json',
'packages/notebook-extension/package.json',
'packages/terminal-extension/package.json',
'packages/tree-extension/package.json',
'packages/tree/package.json',
'packages/ui-components/package.json',
];

const DEPENDENCY_GROUP = '@jupyterlab';

async function updatePackageJson(newVersion: string): Promise<void> {
const url = `https://raw.githubusercontent.com/jupyterlab/jupyterlab/v${newVersion}/jupyterlab/staging/package.json`;
const response = await fetch(url);

if (!response.ok) {
const errorMessage = `Failed to fetch package.json from ${url}. HTTP status code: ${response.status}`;
throw new Error(errorMessage);
}

const newPackageJson = await response.json();

for (const packageJsonPath of PACKAGE_JSON_PATHS) {
const filePath: string = path.resolve(packageJsonPath);
const existingPackageJson = JSON.parse(fs.readFileSync(filePath, 'utf-8'));

const newDependencies = {
...newPackageJson.devDependencies,
...newPackageJson.resolutions,
};

updateDependencyVersion(existingPackageJson, newDependencies);

fs.writeFileSync(filePath, JSON.stringify(existingPackageJson, null, 2));
itsmevichu marked this conversation as resolved.
Show resolved Hide resolved
}
}

function updateDependencyVersion(existingJson: any, newJson: any): void {
if (!existingJson) {
return;
}

const sectionPaths: string[] = [
'resolutions',
'dependencies',
'devDependencies',
];

for (const section of sectionPaths) {
if (!existingJson[section]) {
continue;
}

const updated = existingJson[section];

for (const [pkg, version] of Object.entries<string>(
existingJson[section]
)) {
if (pkg.startsWith(DEPENDENCY_GROUP) && pkg in newJson) {
if (version[0] === '^' || version[0] === '~') {
updated[pkg] = version[0] + absoluteVersion(newJson[pkg]);
} else {
updated[pkg] = absoluteVersion(newJson[pkg]);
}
}
}
}
}

function absoluteVersion(version: string): string {
if (version.length > 0 && (version[0] === '^' || version[0] === '~')) {
return version.substring(1);
}
return version;
}

async function upgradeLabDependencies(): Promise<void> {
const args: string[] = process.argv.slice(2);

if (args.length !== 2 || args[0] !== '--set-version') {
console.error('Usage: node script.js --set-version <version>');
process.exit(1);
}

const newVersion: string = args[1];
await updatePackageJson(newVersion);
}

upgradeLabDependencies();
Loading