From dbc615e33f21211f5ecc0c8997d5713a7c4b8162 Mon Sep 17 00:00:00 2001 From: Vishnutheep B Date: Tue, 27 Feb 2024 08:10:40 +0000 Subject: [PATCH] workflow to upgrade JupyterLab dependencies --- .../upgrade-juypterlab-dependencies.yml | 86 +++++++++++++++++++ scripts/get-latest-lab-version.py | 44 ++++++++++ scripts/upgrade-lab-dependencies.py | 82 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 .github/workflows/upgrade-juypterlab-dependencies.yml create mode 100644 scripts/get-latest-lab-version.py create mode 100644 scripts/upgrade-lab-dependencies.py diff --git a/.github/workflows/upgrade-juypterlab-dependencies.yml b/.github/workflows/upgrade-juypterlab-dependencies.yml new file mode 100644 index 0000000000..39e380f164 --- /dev/null +++ b/.github/workflows/upgrade-juypterlab-dependencies.yml @@ -0,0 +1,86 @@ +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 requests + sudo apt-get install hub + + - name: Install Node + uses: actions/setup-node@v2 + with: + node-version: '20.x' + + - name: Install npm dependencies + run: | + npm install --global yarn + yarn install + + - name: Check for new releases and update + shell: bash + run: | + set -eux + for version in ${{ inputs.version || env.version_tag }} + do + export LATEST=$(python scripts/get-latest-lab-version.py --set-version $version) + done + + echo "latest=${LATEST}" >> $GITHUB_ENV + python scripts/upgrade-lab-dependencies.py --set-version ${LATEST} + if [[ ! -z "$(git status --porcelain package.json)" ]]; then + yarn install + fi + + - name: Create a PR + shell: bash + env: + GITHUB_USER: ${{ secrets.G_USER }} + GITHUB_TOKEN: ${{ secrets.G_TOKEN }} + run: | + set -eux + # if resulted in any change: + export LATEST=${{ env.latest }} + if [[ ! -z "$(git status --porcelain package.json)" ]]; then + export BRANCH_NAME=update-to-v${LATEST} + + # this will fail if the branch already exists which means we won't have duplicate PRs + git checkout -b "${BRANCH_NAME}" + git config user.name "JupyterLab Desktop 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 diff --git a/scripts/get-latest-lab-version.py b/scripts/get-latest-lab-version.py new file mode 100644 index 0000000000..b74be46169 --- /dev/null +++ b/scripts/get-latest-lab-version.py @@ -0,0 +1,44 @@ +from urllib.request import urlopen +import json +import argparse + + +REPOSITORY = "jupyterlab" +ORGANIZATION = "jupyterlab" + + +def extract_version_from_releases(releases, version_tag): + for release in releases: + tag_name = release['tag_name'] + if (version_tag == "latest"): + if(not release['prerelease'] and not release['draft']): + return tag_name + + elif (version_tag == tag_name): + return tag_name + return None + + +def find_version(owner, repository, version_tag): + """Find latest stable release on GitHub for given repository.""" + endpoint = f"https://api.github.com/repos/{owner}/{repository}/releases" + releases = json.loads(urlopen(endpoint).read()) + + version = extract_version_from_releases(releases,version_tag) + + if version is None: + raise ValueError('Invalid release tag') + if not version.startswith('v'): + raise ValueError('Unexpected release tag name format: does not start with v') + return version[1:] + +def main(): + parser = argparse.ArgumentParser(description='Update dependencies in package.json.') + parser.add_argument('--set-version', dest='version_tag', type=str, required=True, help='Set version tag') + + args = parser.parse_args() + print(find_version(owner=ORGANIZATION,repository=REPOSITORY,version_tag=args.version_tag)) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/scripts/upgrade-lab-dependencies.py b/scripts/upgrade-lab-dependencies.py new file mode 100644 index 0000000000..11dfa92106 --- /dev/null +++ b/scripts/upgrade-lab-dependencies.py @@ -0,0 +1,82 @@ +import json +import argparse +import requests + +PACKAGE_JSON_PATHS = [ + "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", +] + +DEPENDENCY_NAME = "@jupyterlab" + +def update_package_json(new_version): + url = f'https://api.github.com/repos/{owner}/{repository}/releases' + + response = requests.get(url) + + if response.status_code != 200: + print(f"Failed to fetch package.json from {url}. HTTP status code: {response.status_code}") + return + + new_package_json = response.json() + + for path in PACKAGE_JSON_PATHS: + with open(path, 'r') as package_json_file: + existing_package_json = json.load(package_json_file) + + new_dependencies = {**new_package_json.get('devDependencies', {}), **new_package_json.get('resolutions', {})} + update_dependencies(existing_package_json, new_dependencies) + + with open(path, 'w') as file: + json.dump(existing_package_json, file, indent=2) + file.write('\n') + + +def update_dependencies(existing_json, new_json): + if(existing_json == None): + return + + section_paths = ['resolutions','dependencies', 'devDependencies'] + + for section in section_paths: + if(existing_json.get(section) == None): + continue + + updated = existing_json.get(section) + for package, version in existing_json.get(section).items(): + if(package.startswith(DEPENDENCY_NAME) and package in new_json): + # To maintaing the existing prefix ~ or ^ + if version[0] in ('^','~'): + updated[package] = version[0] + absolute_version(new_json[package]) + else: + updated[package] = absolute_version(new_json[package]) + +def absolute_version(version): + if len(version) > 0 and version[0] in ('^','~'): + return version[1:]; + return version; + + +def main(): + parser = argparse.ArgumentParser(description='Update dependencies in package.json.') + parser.add_argument('--set-version', dest='new_version', type=str, required=True, help='New version to set for JupyterLab dependencies') + + args = parser.parse_args() + update_package_json(args.new_version) + + +if(__name__ == "__main__"): + main() \ No newline at end of file