From 2d1ca36a97a5fb5571a00b55a54099c0f273c6d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Thu, 26 Oct 2023 14:06:56 +0200 Subject: [PATCH] Set up releaser (#710) * Set up releaser * Fix CI * Add version to root package * Debug * Some fixes * JupyterLab is required to build the lab extension * Ignore duplication warning for wheel * Fix package-lock * Try using releaser run * Apparently --yes cli option matters * Fix bump script * Fix package-lock --- .github/workflows/check-release.yml | 24 ++++++ .github/workflows/enforce-label.yml | 13 +++ .github/workflows/prep-release.yml | 41 ++++++++++ .github/workflows/publish-release.yml | 53 ++++++++++++ CHANGELOG.md | 5 ++ RELEASE.md | 112 ++++++++++++++++++++++++++ docs/source/installing.rst | 20 +---- docs/source/nodevenv.rst | 4 +- package-lock.json | 3 + package.json | 1 + packages/webapp/package.json | 1 + pyproject.toml | 15 ++++ scripts/bump_version.py | 75 +++++++++++++++++ 13 files changed, 347 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/check-release.yml create mode 100644 .github/workflows/enforce-label.yml create mode 100644 .github/workflows/prep-release.yml create mode 100644 .github/workflows/publish-release.yml create mode 100644 CHANGELOG.md create mode 100644 RELEASE.md create mode 100644 scripts/bump_version.py diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml new file mode 100644 index 00000000..99048f84 --- /dev/null +++ b/.github/workflows/check-release.yml @@ -0,0 +1,24 @@ +name: Check Release +on: + push: + branches: ["master", "*"] + pull_request: + branches: ["*"] + +jobs: + check_release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + - name: Check Release + uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload Distributions + uses: actions/upload-artifact@v3 + with: + name: nbdime-releaser-dist-${{ '{{ github.run_number }}' }} + path: .jupyter_releaser_checkout/dist diff --git a/.github/workflows/enforce-label.yml b/.github/workflows/enforce-label.yml new file mode 100644 index 00000000..725feab5 --- /dev/null +++ b/.github/workflows/enforce-label.yml @@ -0,0 +1,13 @@ +name: Enforce PR label + +on: + pull_request: + types: [labeled, unlabeled, opened, edited, synchronize] +jobs: + enforce-label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: enforce-triage-label + uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 diff --git a/.github/workflows/prep-release.yml b/.github/workflows/prep-release.yml new file mode 100644 index 00000000..6f092810 --- /dev/null +++ b/.github/workflows/prep-release.yml @@ -0,0 +1,41 @@ +name: "Step 1: Prep Release" +on: + workflow_dispatch: + inputs: + version_spec: + description: "New Version Specifier" + default: "next" + required: false + branch: + description: "The branch to target" + required: false + post_version_spec: + description: "Post Version Specifier" + required: false + since: + description: "Use PRs with activity since this date or git reference" + required: false + since_last_stable: + description: "Use PRs with activity since the last stable git tag" + required: false + type: boolean +jobs: + prep_release: + runs-on: ubuntu-latest + steps: + - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Prep Release + id: prep-release + uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 + with: + token: ${{ secrets.ADMIN_GITHUB_TOKEN }} + version_spec: ${{ github.event.inputs.version_spec }} + post_version_spec: ${{ github.event.inputs.post_version_spec }} + branch: ${{ github.event.inputs.branch }} + since: ${{ github.event.inputs.since }} + since_last_stable: ${{ github.event.inputs.since_last_stable }} + + - name: "** Next Step **" + run: | + echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 00000000..288b33c6 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,53 @@ +name: "Step 2: Publish Release" +on: + workflow_dispatch: + inputs: + branch: + description: "The target branch" + required: false + release_url: + description: "The URL of the draft GitHub release" + required: false + steps_to_skip: + description: "Comma separated list of steps to skip" + required: false + +jobs: + publish_release: + runs-on: ubuntu-latest + permissions: + # This is useful if you want to use PyPI trusted publisher + # and NPM provenance + id-token: write + steps: + - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Populate Release + id: populate-release + uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 + with: + token: ${{ secrets.ADMIN_GITHUB_TOKEN }} + branch: ${{ github.event.inputs.branch }} + release_url: ${{ github.event.inputs.release_url }} + steps_to_skip: ${{ github.event.inputs.steps_to_skip }} + + - name: Finalize Release + id: finalize-release + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 + with: + token: ${{ secrets.ADMIN_GITHUB_TOKEN }} + release_url: ${{ steps.populate-release.outputs.release_url }} + + - name: "** Next Step **" + if: ${{ success() }} + run: | + echo "Verify the final release" + echo ${{ steps.finalize-release.outputs.release_url }} + + - name: "** Failure Message **" + if: ${{ failure() }} + run: | + echo "Failed to Publish the Draft Release Url:" + echo ${{ steps.populate-release.outputs.release_url }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2d352af4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + + + + diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..7d582894 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,112 @@ +# Making a new release of nbdime + +The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). + +## Manual release + +### Python package + +This extension can be distributed as Python packages. All of the Python +packaging instructions are in the `pyproject.toml` file to wrap your extension in a +Python package. Before generating a package, you first need to install some tools: + +```bash +pip install build twine hatch +``` + +Bump the version using the custom script: + +```bash +python scripts/bump_version.py +``` + +> _\_ can be a segment like `major`, `minor`, `patch`, `alpha`,... + +Make sure to clean up all the development files before building the package: + +```bash +jlpm clean:all +``` + +You could also clean up the local git repository: + +```bash +git clean -dfX +``` + +To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: + +```bash +python -m build +``` + +> `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. + +Then to upload the package to PyPI, do: + +```bash +twine upload dist/* +``` + +### NPM package + +To publish the frontend part of the extension as a NPM package, do: + +```bash +npm login +npm publish --access public +``` + +## Automated releases with the Jupyter Releaser + +The extension repository should already be compatible with the Jupyter Releaser. + +Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. + +Here is a summary of the steps to cut a new release: + +- Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository: + - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) + - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens) +- Set up PyPI + +
Using PyPI trusted publisher (modern way) + +- Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) + - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank. +- Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/)) + +
+ +
Using PyPI token (legacy way) + +- If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. + +- You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. + + - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: + + ```text + owner1/repo1,token1 + owner2/repo2,token2 + ``` + + If you have multiple Python packages in the same repository, you can point to them as follows: + + ```text + owner1/repo1/path/to/package1,token1 + owner1/repo1/path/to/package2,token2 + ``` + +
+ +- Go to the Actions panel +- Run the "Step 1: Prep Release" workflow +- Check the draft changelog +- Run the "Step 2: Publish Release" workflow + +## Publishing to `conda-forge` + +If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html + +Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. \ No newline at end of file diff --git a/docs/source/installing.rst b/docs/source/installing.rst index 4d723c49..955b8b7e 100644 --- a/docs/source/installing.rst +++ b/docs/source/installing.rst @@ -44,7 +44,6 @@ and nbdime's web-based viewers depend on the following Node.js packages: - codemirror - json-stable-stringify - - jupyter-js-services - jupyterlab - lumino @@ -90,31 +89,16 @@ Installing Jupyter extensions If you want to use the development version of the notebook and lab extensions, you will also have to run the following commands after the pip dev install:: - jupyter serverextension enable --py nbdime --sys-prefix # if developing for jupyter notebook - jupyter server extension enable nbdime # if developing for jupyter lab or nbclassic - jupyter nbextension install --py nbdime --sys-prefix [--sym-link] - jupyter nbextension enable --py nbdime --sys-prefix - - jupyter labextension link ./packages/nbdime --no-build - jupyter labextension install ./packages/labextension + jupyter labextension develop --overwrite . If you do any changes to the front-end code, run :command:`npm run build` from the -repoistory root to rebuild the extensions. If you make any changes to the +repository root to rebuild the extensions. If you make any changes to the server extension, you will have to restart the server to pick up the changes! -.. note:: - - The optional ``--sym-link`` flag for :command:`jupyter nbextension install` allows - the notebook frontend to pick up a newly built version of the extension on - a page refresh. For details on the other flags, see - :doc:`extensions`. - - - .. toctree:: :hidden: diff --git a/docs/source/nodevenv.rst b/docs/source/nodevenv.rst index d088f5a3..44c5e6be 100644 --- a/docs/source/nodevenv.rst +++ b/docs/source/nodevenv.rst @@ -5,7 +5,7 @@ The following steps will: create a virtualenv, named ``myenv``, in the current directory; activate the virtualenv; and install npm inside the virtualenv using :command:`nodeenv`:: - python3 -m venv myenv # For Python 2: python2 -m virtualenv myenv + python -m venv myenv source myenv/bin/activate pip install nodeenv nodeenv -p @@ -15,7 +15,7 @@ dependencies using :command:`pip`. For example with Python 3.5, the steps with output are:: - $ python3 -m venv myenv + $ python -m venv myenv $ source myenv/bin/activate (myenv) $ pip install nodeenv Collecting nodeenv diff --git a/package-lock.json b/package-lock.json index 35ed83c2..92de4f8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,10 +1,12 @@ { "name": "nbdime-top-repo", + "version": "4.0.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nbdime-top-repo", + "version": "4.0.0-alpha.0", "workspaces": [ "packages/*" ], @@ -22415,6 +22417,7 @@ }, "packages/webapp": { "name": "nbdime-webapp", + "version": "4.0.0-alpha.0", "license": "BSD-3-Clause", "dependencies": { "@fortawesome/fontawesome-free": "^5.12.0", diff --git a/package.json b/package.json index 69a100eb..ae954296 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "nbdime-top-repo", + "version": "4.0.0-alpha.0", "private": true, "workspaces": [ "packages/*" diff --git a/packages/webapp/package.json b/packages/webapp/package.json index fd1ae8cc..09f40f2b 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -1,5 +1,6 @@ { "name": "nbdime-webapp", + "version": "4.0.0-alpha.0", "private": true, "license": "BSD-3-Clause", "main": "static/nbdime.js", diff --git a/pyproject.toml b/pyproject.toml index 5c175b68..487b4669 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,9 @@ docs = [ "sphinx_rtd_theme", ] +[tool.check-wheel-contents] +ignore = ["W002"] + [tool.hatch.version] path = "nbdime/_version.py" @@ -129,6 +132,18 @@ skip-if-exists = [ source_dir = "packages" build_dir = "nbdime/labextension" +[tool.jupyter-releaser.options] +version-cmd = "python scripts/bump_version.py --force" + +[tool.jupyter-releaser.hooks] +before-bump-version = [ + "npm install" +] +before-build-npm = [ + "python -m pip install 'jupyterlab>=4.0.0,<5'", + "npm run build" +] + [tool.pytest.ini_options] testpaths = "nbdime/tests" norecursedirs = "node_modules" diff --git a/scripts/bump_version.py b/scripts/bump_version.py new file mode 100644 index 00000000..83eec63c --- /dev/null +++ b/scripts/bump_version.py @@ -0,0 +1,75 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import argparse +import shlex +import sys +from pathlib import Path +from subprocess import check_output, check_call +try: + from jupyter_releaser import run +except ImportError: + def run(cmd, **kwargs): + check_call(shlex.split(cmd), encoding="utf-8", **kwargs) + + +LERNA_CMD = "npx lerna version --no-push --no-git-tag-version" + + +def install_dependencies() -> None: + pkgs = [] + try: + import hatch + except ImportError: + pkgs.append("hatch") + + if pkgs: + run(f"{sys.executable} -m pip install {' '.join(pkgs)}") + + +def bump(force: bool, spec: str) -> None: + install_dependencies() + + HERE = Path(__file__).parent.parent.resolve() + output = check_output( + shlex.split("git status --porcelain"), cwd=HERE, encoding="utf-8" + ) + if len(output) > 0: + print(output) + raise Exception("Must be in a clean git state with no untracked files") + + print(f"Executing 'python -m hatch version {spec}'...") + run( + f"{sys.executable} -m hatch version {spec}", cwd=HERE + ) + + # convert the Python version + lerna_cmd = LERNA_CMD + if spec in ["alpha", "a", "beta", "b", "rc"]: + js_spec = "--force-publish" + spec = "prerelease" + elif spec == "release": + js_spec = "--conventional-commits --no-changelog --conventional-graduate" + spec = "" + else: + js_spec = f"--force-publish" + + # bump the JS packages + if force: + # This needs to be the latest option for weird reason + js_spec += " -y" + lerna_cmd += f" {js_spec} {spec}" + print(f"Executing '{lerna_cmd}'...") + run(lerna_cmd, cwd=HERE) + + print(f"Changed made:") + run("git diff", cwd=HERE) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser("bump_version", "Bump package version") + parser.add_argument("--force", action="store_true") + parser.add_argument("spec") + + args = parser.parse_args() + bump(args.force, args.spec)