diff --git a/.github/workflows/upgrade-juypterlab-dependencies.yml b/.github/workflows/upgrade-juypterlab-dependencies.yml new file mode 100644 index 0000000000..987d16c85e --- /dev/null +++ b/.github/workflows/upgrade-juypterlab-dependencies.yml @@ -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 'jupyterlab-bot@users.noreply.github.com' + + 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 diff --git a/buildutils/src/get-latest-lab-version.ts b/buildutils/src/get-latest-lab-version.ts new file mode 100644 index 0000000000..166e035a59 --- /dev/null +++ b/buildutils/src/get-latest-lab-version.ts @@ -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 { + 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 { + const args: string[] = process.argv.slice(2); + if (args.length !== 2 || args[0] !== '--set-version') { + console.error('Usage: node script.js --set-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(); diff --git a/buildutils/src/upgrade-lab-dependencies.ts b/buildutils/src/upgrade-lab-dependencies.ts new file mode 100644 index 0000000000..003243f8a8 --- /dev/null +++ b/buildutils/src/upgrade-lab-dependencies.ts @@ -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 { + 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) + '\n'); + } +} + +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( + 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 { + const args: string[] = process.argv.slice(2); + + if (args.length !== 2 || args[0] !== '--set-version') { + console.error('Usage: node script.js --set-version '); + process.exit(1); + } + + const newVersion: string = args[1]; + await updatePackageJson(newVersion); +} + +upgradeLabDependencies();