├── .github
├── actions
│ ├── check-release
│ │ └── action.yml
│ ├── finalize-release
│ │ └── action.yml
│ ├── install-releaser
│ │ └── action.yml
│ ├── populate-release
│ │ └── action.yml
│ ├── prep-release
│ │ └── action.yml
│ └── publish-changelog
│ │ └── action.yml
├── dependabot.yml
├── scripts
│ ├── bump_tag.sh
│ └── install-releaser.sh
└── workflows
│ ├── check-release.yml
│ ├── enforce-label.yml
│ ├── generate-changelog.yml
│ ├── prep-release.yml
│ ├── prep-self-release.yml
│ ├── publish-changelog.yml
│ ├── publish-release.yml
│ ├── publish-self-release.yml
│ └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── RELEASE.md
├── docs
└── source
│ ├── _static
│ ├── custom.css
│ └── images
│ │ └── logo
│ │ ├── favicon.svg
│ │ └── logo.svg
│ ├── conf.py
│ ├── faq
│ └── index.md
│ ├── get_started
│ ├── generate_changelog.md
│ ├── index.md
│ ├── making_release_from_releaser.md
│ └── making_release_from_repo.md
│ ├── how_to_guides
│ ├── convert_repo_from_releaser.md
│ ├── convert_repo_from_repo.md
│ ├── index.md
│ ├── maintain_fork.md
│ └── write_config.md
│ ├── images
│ ├── draft_github_release.png
│ ├── final_github_release.png
│ ├── fork_fetch.png
│ ├── generate_changelog.png
│ ├── prep_release.png
│ ├── prep_release_next_step.png
│ ├── prep_release_repo.png
│ ├── publish_release.png
│ ├── publish_release_next_step.png
│ └── publish_release_repo.png
│ ├── index.md
│ └── reference
│ ├── api_docs.md
│ ├── changelog.md
│ ├── configuration.md
│ ├── index.md
│ ├── releaser_cli.md
│ └── theory.md
├── example-workflows
├── full-release.yml
├── prep-release.yml
├── publish-changelog.yml
└── publish-release.yml
├── jupyter_releaser
├── __init__.py
├── __main__.py
├── actions
│ ├── __init__.py
│ ├── common.py
│ ├── finalize_release.py
│ ├── generate_changelog.py
│ ├── populate_release.py
│ ├── prep_release.py
│ └── publish_changelog.py
├── changelog.py
├── cli.py
├── lib.py
├── mock_github.py
├── npm.py
├── py.typed
├── python.py
├── schema.json
├── tee.py
└── util.py
├── pyproject.toml
└── tests
├── __init__.py
├── conftest.py
├── test_changelog.py
├── test_cli.py
├── test_functions.py
├── test_mock_github.py
└── util.py
/.github/actions/check-release/action.yml:
--------------------------------------------------------------------------------
1 | name: "Check Release"
2 | description: "Run through a dry run release cycle"
3 | inputs:
4 | token:
5 | description: "GitHub access token"
6 | required: true
7 | version_spec:
8 | description: "New Version Specifier"
9 | required: false
10 | default: ""
11 | steps_to_skip:
12 | description: "Comma separated list of steps to skip"
13 | required: false
14 | admin_check:
15 | description: "Check if the user is a repo admin"
16 | required: false
17 | default: "true"
18 | shell:
19 | description: "The shell being used for the action steps"
20 | required: false
21 | default: bash -eux {0}
22 | runs:
23 | using: "composite"
24 | steps:
25 | - shell: ${{ inputs.shell }}
26 | id: install-releaser
27 | run: |
28 | cd "${{ github.action_path }}/../../scripts"
29 | bash install-releaser.sh
30 |
31 | - id: prep-release
32 | shell: ${{ inputs.shell }}
33 | run: |
34 | export RH_DRY_RUN="true"
35 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
36 | export RH_VERSION_SPEC=${{ inputs.version_spec }}
37 | export RH_STEPS_TO_SKIP=${{ inputs.steps_to_skip }}
38 | export RH_ADMIN_CHECK=${{ inputs.admin_check }}
39 | python -m jupyter_releaser.actions.prep_release
40 |
41 | - id: populate-release
42 | shell: ${{ inputs.shell }}
43 | run: |
44 | export RH_DRY_RUN="true"
45 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
46 | export RH_RELEASE_URL=${{ steps.prep-release.outputs.release_url }}
47 | export RH_STEPS_TO_SKIP=${{ inputs.steps_to_skip }}
48 | export RH_ADMIN_CHECK=${{ inputs.admin_check }}
49 | export YARN_UNSAFE_HTTP_WHITELIST=0.0.0.0
50 | python -m jupyter_releaser.actions.populate_release
51 |
52 | - id: finalize-release
53 | shell: ${{ inputs.shell }}
54 | run: |
55 | export RH_DRY_RUN="true"
56 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
57 | export RH_RELEASE_URL=${{ steps.populate-release.outputs.release_url }}
58 | export RH_STEPS_TO_SKIP=${{ inputs.steps_to_skip }}
59 | export RH_ADMIN_CHECK=${{ inputs.admin_check }}
60 | python -m jupyter_releaser.actions.finalize_release
61 |
--------------------------------------------------------------------------------
/.github/actions/finalize-release/action.yml:
--------------------------------------------------------------------------------
1 | name: "Finalize Release"
2 | description: "Publish assets and finalize GitHub release"
3 | inputs:
4 | token:
5 | description: "GitHub access token"
6 | required: true
7 | target:
8 | description: "The owner/repo GitHub target"
9 | required: false
10 | branch:
11 | description: "The target branch"
12 | required: false
13 | release_url:
14 | description: "The full url to the GitHub release page"
15 | required: false
16 | dry_run:
17 | description: "If set, do not push permanent changes"
18 | default: "false"
19 | required: false
20 | steps_to_skip:
21 | description: "Comma separated list of steps to skip"
22 | required: false
23 | admin_check:
24 | description: "Check if the user is a repo admin"
25 | required: false
26 | default: "true"
27 | shell:
28 | description: "The shell being used for the action steps"
29 | required: false
30 | default: bash -eux {0}
31 |
32 | outputs:
33 | release_url:
34 | description: "The html URL of the GitHub release"
35 | value: ${{ steps.finalize-release.outputs.release_url }}
36 | pr_url:
37 | description: "The html URL of the forwardport PR if applicable"
38 | value: ${{ steps.finalize-release.outputs.pr_url }}
39 |
40 | runs:
41 | using: "composite"
42 | steps:
43 | - name: install-releaser
44 | shell: ${{ inputs.shell }}
45 | run: |
46 | cd "${{ github.action_path }}/../../scripts"
47 | bash install-releaser.sh
48 |
49 | - id: finalize-release
50 | shell: ${{ inputs.shell }}
51 | run: |
52 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
53 | export GITHUB_ACTOR=${{ github.triggering_actor }}
54 | export RH_REPOSITORY=${{ inputs.target }}
55 | export RH_DRY_RUN=${{ inputs.dry_run }}
56 | export RH_RELEASE_URL=${{ inputs.release_url }}
57 | export RH_STEPS_TO_SKIP=${{ inputs.steps_to_skip }}
58 | export RH_BRANCH=${{ inputs.branch }}
59 | export RH_ADMIN_CHECK=${{ inputs.admin_check }}
60 | python -m jupyter_releaser.actions.finalize_release
61 |
62 | - if: ${{ success() }}
63 | shell: ${{ inputs.shell }}
64 | run: |
65 | echo "## Next Step" >> $GITHUB_STEP_SUMMARY
66 | echo "Verify the final release" >> $GITHUB_STEP_SUMMARY
67 | echo ${{ steps.finalize-release.outputs.release_url }} >> $GITHUB_STEP_SUMMARY
68 | if [ ! -z "${{ steps.finalize-release.outputs.pr_url }}" ]; then
69 | echo "Merge the forwardport PR"
70 | echo ${{ steps.finalize-release.outputs.pr_url }}
71 | echo "Merge the forwardport PR" >> $GITHUB_STEP_SUMMARY
72 | echo ${{ steps.finalize-release.outputs.pr_url }} >> $GITHUB_STEP_SUMMARY
73 | fi
74 |
75 | - if: ${{ failure() }}
76 | shell: ${{ inputs.shell }}
77 | run: |
78 | echo "## Failure Message" >> $GITHUB_STEP_SUMMARY
79 | echo ":x: Failed to Publish the Draft Release Url:" >> $GITHUB_STEP_SUMMARY
80 | echo ${{ inputs.release_url }} >> $GITHUB_STEP_SUMMARY
81 |
--------------------------------------------------------------------------------
/.github/actions/install-releaser/action.yml:
--------------------------------------------------------------------------------
1 | name: "Install Releaser"
2 | description: "Ensure Releaser is Installed"
3 | runs:
4 | using: "composite"
5 | steps:
6 | - shell: bash
7 | id: install-releaser
8 | run: |
9 | cd "${{ github.action_path }}/../../scripts"
10 | bash install-releaser.sh
11 |
--------------------------------------------------------------------------------
/.github/actions/populate-release/action.yml:
--------------------------------------------------------------------------------
1 | name: "Populate Release"
2 | description: "Populate the Draft GitHub Release"
3 | inputs:
4 | token:
5 | description: "GitHub access token"
6 | required: true
7 | target:
8 | description: "The owner/repo GitHub target"
9 | required: false
10 | branch:
11 | description: "The target branch"
12 | required: false
13 | release_url:
14 | description: "The full url to the GitHub release page"
15 | required: false
16 | dry_run:
17 | description: "If set, do not push permanent changes"
18 | default: "false"
19 | required: false
20 | steps_to_skip:
21 | description: "Comma separated list of steps to skip"
22 | required: false
23 | admin_check:
24 | description: "Check if the user is a repo admin"
25 | required: false
26 | default: "true"
27 | shell:
28 | description: "The shell being used for the action steps"
29 | required: false
30 | default: bash -eux {0}
31 |
32 | outputs:
33 | release_url:
34 | description: "The html URL of the draft GitHub release"
35 | value: ${{ steps.populate-release.outputs.release_url }}
36 |
37 | runs:
38 | using: "composite"
39 | steps:
40 | - name: install-releaser
41 | shell: ${{ inputs.shell }}
42 | run: |
43 | cd "${{ github.action_path }}/../../scripts"
44 | bash install-releaser.sh
45 |
46 | - id: populate-release
47 | shell: ${{ inputs.shell }}
48 | run: |
49 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
50 | export GITHUB_ACTOR=${{ github.triggering_actor }}
51 | export RH_REPOSITORY=${{ inputs.target }}
52 | export RH_DRY_RUN=${{ inputs.dry_run }}
53 | export RH_STEPS_TO_SKIP=${{ inputs.steps_to_skip }}
54 | export RH_RELEASE_URL=${{ inputs.release_url }}
55 | export RH_BRANCH=${{ inputs.branch }}
56 | export RH_ADMIN_CHECK=${{ inputs.admin_check }}
57 | python -m jupyter_releaser.actions.populate_release
58 |
59 | - if: ${{ failure() }}
60 | shell: ${{ inputs.shell }}
61 | run: |
62 | echo "## Failure Message" >> $GITHUB_STEP_SUMMARY
63 | echo ":x: Failed to Publish the Draft Release Url:" >> $GITHUB_STEP_SUMMARY
64 | echo ${{ steps.populate-release.outputs.release_url }} >> $GITHUB_STEP_SUMMARY
65 |
--------------------------------------------------------------------------------
/.github/actions/prep-release/action.yml:
--------------------------------------------------------------------------------
1 | name: "Prep Release"
2 | description: "Start the Release Process"
3 | inputs:
4 | token:
5 | description: "GitHub access token"
6 | required: true
7 | target:
8 | description: "The owner/repo GitHub target"
9 | required: false
10 | version_spec:
11 | description: "New Version Specifier"
12 | default: "next"
13 | required: false
14 | branch:
15 | description: "The branch to target"
16 | required: false
17 | post_version_spec:
18 | description: "Post Version Specifier"
19 | required: false
20 | dry_run:
21 | description: "If set, do not make a PR"
22 | default: "false"
23 | required: false
24 | silent:
25 | description: "Set a placeholder in the changelog and don't publish the release."
26 | required: false
27 | since:
28 | description: "Use PRs with activity since this date or git reference"
29 | required: false
30 | since_last_stable:
31 | description: "Use PRs with activity since the last stable git tag"
32 | required: false
33 | admin_check:
34 | description: "Check if the user is a repo admin"
35 | required: false
36 | default: "true"
37 | shell:
38 | description: "The shell being used for the action steps"
39 | required: false
40 | default: bash -eux {0}
41 | outputs:
42 | release_url:
43 | description: "The html URL of the draft GitHub release"
44 | value: ${{ steps.prep-release.outputs.release_url }}
45 | runs:
46 | using: "composite"
47 | steps:
48 | - name: install-releaser
49 | shell: ${{ inputs.shell }}
50 | run: |
51 | cd "${{ github.action_path }}/../../scripts"
52 | bash install-releaser.sh
53 |
54 | - id: prep-release
55 | shell: ${{ inputs.shell }}
56 | run: |
57 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
58 | export GITHUB_ACTOR=${{ github.triggering_actor }}
59 | export RH_REPOSITORY=${{ inputs.target }}
60 | if [ ! -z ${{ inputs.branch }} ]; then
61 | export RH_BRANCH=${{ inputs.branch }}
62 | fi
63 | export RH_VERSION_SPEC=${{ inputs.version_spec }}
64 | export RH_POST_VERSION_SPEC=${{ inputs.post_version_spec }}
65 | export RH_DRY_RUN=${{ inputs.dry_run }}
66 | export RH_SILENT=${{ inputs.silent }}
67 | export RH_SINCE=${{ inputs.since }}
68 | export RH_SINCE_LAST_STABLE=${{ inputs.since_last_stable }}
69 | export RH_ADMIN_CHECK=${{ inputs.admin_check }}
70 |
71 | python -m jupyter_releaser.actions.prep_release
72 |
73 | - shell: ${{ inputs.shell }}
74 | run: |
75 | echo "## Next Step" >> $GITHUB_STEP_SUMMARY
76 | echo "(Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY
77 | echo "Run Step 2: Publish Release workflow"
78 |
--------------------------------------------------------------------------------
/.github/actions/publish-changelog/action.yml:
--------------------------------------------------------------------------------
1 | name: "Publish Changelog"
2 | description: "Remove silent placeholder entries in the changelog file."
3 | inputs:
4 | token:
5 | description: "GitHub access token"
6 | required: true
7 | target:
8 | description: "The owner/repo GitHub target"
9 | required: false
10 | branch:
11 | description: "The branch to target"
12 | required: false
13 | dry_run:
14 | description: "If set, do not make a PR"
15 | default: "false"
16 | required: false
17 | shell:
18 | description: "The shell being used for the action steps"
19 | required: false
20 | default: bash -eux {0}
21 | outputs:
22 | pr_url:
23 | description: "The html URL of the draft GitHub release"
24 | value: ${{ steps.publish-changelog.outputs.pr_url }}
25 | runs:
26 | using: "composite"
27 | steps:
28 | - name: install-releaser
29 | shell: ${{ inputs.shell }}
30 | run: |
31 | cd "${{ github.action_path }}/../../scripts"
32 | bash install-releaser.sh
33 |
34 | - id: publish-changelog
35 | shell: ${{ inputs.shell }}
36 | run: |
37 | export GITHUB_ACCESS_TOKEN=${{ inputs.token }}
38 | export GITHUB_ACTOR=${{ github.triggering_actor }}
39 | export RH_REPOSITORY=${{ inputs.target }}
40 | if [ ! -z ${{ inputs.branch }} ]; then
41 | export RH_BRANCH=${{ inputs.branch }}
42 | fi
43 | export RH_DRY_RUN=${{ inputs.dry_run }}
44 | export RH_ADMIN_CHECK=false
45 |
46 | python -m jupyter_releaser.actions.publish_changelog
47 |
48 | - shell: ${{ inputs.shell }}
49 | run: |
50 | echo "## Next Step" >> $GITHUB_STEP_SUMMARY
51 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}" >> $GITHUB_STEP_SUMMARY
52 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | groups:
8 | actions:
9 | patterns:
10 | - "*"
11 | - package-ecosystem: "pip"
12 | directory: "/"
13 | schedule:
14 | interval: "weekly"
15 | groups:
16 | actions:
17 | patterns:
18 | - "*"
19 |
--------------------------------------------------------------------------------
/.github/scripts/bump_tag.sh:
--------------------------------------------------------------------------------
1 | set -eux
2 |
3 | # Update the v1 tag for GitHub Actions consumers
4 | if [[ ${RH_DRY_RUN:=true} != 'true' ]]; then
5 | git tag -f -a v2 -m "Github Action release"
6 | git push origin -f --tags
7 | fi
8 |
--------------------------------------------------------------------------------
/.github/scripts/install-releaser.sh:
--------------------------------------------------------------------------------
1 | set -eux
2 | # Install Jupyter Releaser if it is not already installed
3 |
4 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
5 |
6 | if ! command -v jupyter-releaser &> /dev/null
7 | then
8 | cd "${SCRIPT_DIR}/../.."
9 | pip install -e .
10 | fi
11 |
--------------------------------------------------------------------------------
/.github/workflows/check-release.yml:
--------------------------------------------------------------------------------
1 | name: Check Release
2 | on:
3 | push:
4 | branches: ["*"]
5 | pull_request:
6 | branches: ["*"]
7 | release:
8 | types: [published]
9 | schedule:
10 | - cron: "0 0 * * *"
11 |
12 | jobs:
13 | check_release:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
18 | - name: Install Dependencies
19 | shell: bash
20 | run: |
21 | pip install -e .
22 | - name: Check Release
23 | uses: ./.github/actions/check-release
24 | with:
25 | token: ${{ secrets.GITHUB_TOKEN }}
26 |
--------------------------------------------------------------------------------
/.github/workflows/enforce-label.yml:
--------------------------------------------------------------------------------
1 | name: Enforce PR label
2 |
3 | on:
4 | pull_request:
5 | types: [labeled, unlabeled, opened, edited, synchronize]
6 | jobs:
7 | enforce-label:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | pull-requests: write
11 | steps:
12 | - name: enforce-triage-label
13 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1
14 |
--------------------------------------------------------------------------------
/.github/workflows/generate-changelog.yml:
--------------------------------------------------------------------------------
1 | name: Generate Changelog
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | target:
6 | description: "Target Owner/Repo"
7 | required: true
8 | branch:
9 | description: "The branch or reference name to filter pull requests by"
10 | required: false
11 | convert_to_rst:
12 | description: "Whether to convert to RST"
13 | required: false
14 | type: boolean
15 | since:
16 | description: "Use PRs with activity since this date or git reference"
17 | required: false
18 | until:
19 | description: "Use PRs with activity until this date or git reference"
20 | required: false
21 | since_last_stable:
22 | description: "Use PRs with activity since the last stable git tag"
23 | required: false
24 | type: boolean
25 | jobs:
26 | generate_changelog:
27 | runs-on: ubuntu-latest
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | python-version: ["3.10"]
32 | steps:
33 | - uses: actions/checkout@v4
34 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
35 | - name: Install Dependencies
36 | shell: bash
37 | run: |
38 | sudo apt-get install pandoc
39 | pip install -e .
40 | pip install pandoc
41 |
42 | - name: Generate the Changelog
43 | env:
44 | GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | run: |
46 | export RH_BRANCH=${{ github.event.inputs.branch }}
47 | export RH_SINCE=${{ github.event.inputs.since }}
48 | export RH_REF=refs/heads/${RH_BRANCH}
49 | export RH_REPOSITORY=${{ github.event.inputs.target }}
50 | export RH_UNTIL=${{ github.event.inputs.until }}
51 | export RH_CONVERT_TO_RST=${{ github.event.inputs.convert_to_rst }}
52 | export RH_SINCE_LAST_STABLE=${{ github.event.inputs.since_last_stable }}
53 | python -m jupyter_releaser.actions.generate_changelog
54 | cat CHANGELOG_ENTRY.md
55 |
56 | - uses: actions/upload-artifact@v4
57 | with:
58 | name: changelog-${{ github.job }}-${{ strategy.job-index }}
59 | path: CHANGELOG_ENTRY.md
60 |
--------------------------------------------------------------------------------
/.github/workflows/prep-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 1: Prep Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | target:
6 | description: "The owner/repo GitHub target"
7 | required: false
8 | version_spec:
9 | description: "New Version Specifier"
10 | default: "next"
11 | required: false
12 | branch:
13 | description: "The branch to target"
14 | required: false
15 | post_version_spec:
16 | description: "Post Version Specifier"
17 | required: false
18 | silent:
19 | description: "Set a placeholder in the changelog and don't publish the release."
20 | required: false
21 | type: boolean
22 | since:
23 | description: "Use PRs with activity since this date or git reference"
24 | required: false
25 | since_last_stable:
26 | description: "Use PRs with activity since the last stable git tag"
27 | required: false
28 | type: boolean
29 | jobs:
30 | prep_release:
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v4
34 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
35 | - name: Install Dependencies
36 | shell: bash
37 | run: |
38 | pip install -e .
39 | - name: Prep Release
40 | id: prep-release
41 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
42 | with:
43 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
44 | version_spec: ${{ github.event.inputs.version_spec }}
45 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
46 | target: ${{ github.event.inputs.target }}
47 | branch: ${{ github.event.inputs.branch }}
48 | silent: ${{ github.event.inputs.silent }}
49 | since: ${{ github.event.inputs.since }}
50 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
51 |
52 | - name: "** Next Step **"
53 | run: |
54 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
55 |
--------------------------------------------------------------------------------
/.github/workflows/prep-self-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 1: Prep Self Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version_spec:
6 | description: "New Version Specifier"
7 | default: "next"
8 | required: false
9 | branch:
10 | description: "The branch to target"
11 | required: false
12 | post_version_spec:
13 | description: "Post Version Specifier"
14 | required: false
15 | silent:
16 | description: "Set a placeholder in the changelog and don't publish the release."
17 | required: false
18 | type: boolean
19 | since:
20 | description: "Use PRs with activity since this date or git reference"
21 | required: false
22 | since_last_stable:
23 | description: "Use PRs with activity since the last stable git tag"
24 | required: false
25 | type: boolean
26 | jobs:
27 | prep_release:
28 | runs-on: ubuntu-latest
29 | permissions:
30 | contents: write
31 | steps:
32 | - uses: actions/checkout@v4
33 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
34 | - name: Install Dependencies
35 | shell: bash
36 | run: |
37 | pip install -e .
38 | - name: Prep Release
39 | id: prep-release
40 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
41 | with:
42 | token: ${{ secrets.GITHUB_TOKEN }}
43 | version_spec: ${{ github.event.inputs.version_spec }}
44 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
45 | target: jupyter-server/jupyter_releaser
46 | branch: ${{ github.event.inputs.branch }}
47 | silent: ${{ github.event.inputs.silent }}
48 | since: ${{ github.event.inputs.since }}
49 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
50 |
51 | - name: "** Next Step **"
52 | run: |
53 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
54 |
--------------------------------------------------------------------------------
/.github/workflows/publish-changelog.yml:
--------------------------------------------------------------------------------
1 | name: "Publish Changelog"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | target:
6 | description: "The owner/repo GitHub target"
7 | required: false
8 | branch:
9 | description: "The branch to target"
10 | required: false
11 |
12 | jobs:
13 | publish_changelog:
14 | runs-on: ubuntu-latest
15 | environment: release
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
19 |
20 | - name: Install Dependencies
21 | shell: bash
22 | run: |
23 | pip install -e .
24 |
25 | - uses: actions/create-github-app-token@v1
26 | id: app-token
27 | with:
28 | app-id: ${{ vars.APP_ID }}
29 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
30 |
31 | - name: Publish changelog
32 | id: publish-changelog
33 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2
34 | with:
35 | token: ${{ steps.app-token.outputs.token }}
36 | target: ${{ github.event.inputs.target }}
37 | branch: ${{ github.event.inputs.branch }}
38 |
39 | - name: "** Next Step **"
40 | run: |
41 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}"
42 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 2: Publish Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | target:
6 | description: "The owner/repo GitHub target"
7 | required: false
8 | branch:
9 | description: "The target branch"
10 | required: false
11 | release_url:
12 | description: "The URL of the draft GitHub release"
13 | required: false
14 | steps_to_skip:
15 | description: "Comma separated list of steps to skip"
16 | required: false
17 |
18 | jobs:
19 | publish_release:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
24 | - name: Install Dependencies
25 | shell: bash
26 | run: |
27 | pip install -e .
28 | - name: Populate Release
29 | id: populate-release
30 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
31 | with:
32 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
33 | target: ${{ github.event.inputs.target }}
34 | branch: ${{ github.event.inputs.branch }}
35 | release_url: ${{ github.event.inputs.release_url }}
36 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
37 |
38 | - name: Finalize Release
39 | id: finalize-release
40 | env:
41 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
42 | PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }}
43 | TWINE_USERNAME: __token__
44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
45 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
46 | with:
47 | token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
48 | target: ${{ github.event.inputs.target }}
49 | release_url: ${{ steps.populate-release.outputs.release_url }}
50 |
51 | - name: "** Next Step **"
52 | if: ${{ success() }}
53 | run: |
54 | echo "Verify the final release"
55 | echo ${{ steps.finalize-release.outputs.release_url }}
56 |
57 | - name: "** Failure Message **"
58 | if: ${{ failure() }}
59 | run: |
60 | echo "Failed to Publish the Draft Release Url:"
61 | echo ${{ steps.populate-release.outputs.release_url }}
62 |
--------------------------------------------------------------------------------
/.github/workflows/publish-self-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 2: Publish Self Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | branch:
6 | description: "The target branch"
7 | required: false
8 | release_url:
9 | description: "The URL of the draft GitHub release"
10 | required: false
11 | steps_to_skip:
12 | description: "Comma separated list of steps to skip"
13 | required: false
14 |
15 | jobs:
16 | publish_release:
17 | runs-on: ubuntu-latest
18 | environment: release
19 | permissions:
20 | id-token: write
21 | steps:
22 | - uses: actions/checkout@v4
23 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
24 | - name: Install Dependencies
25 | shell: bash
26 | run: |
27 | pip install -e .
28 | - uses: actions/create-github-app-token@v1
29 | id: app-token
30 | with:
31 | app-id: ${{ vars.APP_ID }}
32 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
33 | - name: Populate Release
34 | id: populate-release
35 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
36 | with:
37 | token: ${{ steps.app-token.outputs.token }}
38 | target: jupyter-server/jupyter_releaser
39 | branch: ${{ github.event.inputs.branch }}
40 | release_url: ${{ github.event.inputs.release_url }}
41 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
42 |
43 | - name: Finalize Release
44 | id: finalize-release
45 | env:
46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
47 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
48 | with:
49 | token: ${{ steps.app-token.outputs.token }}
50 | target: ${{ github.event.inputs.target }}
51 | release_url: ${{ steps.populate-release.outputs.release_url }}
52 |
53 | - name: "** Next Step **"
54 | if: ${{ success() }}
55 | run: |
56 | echo "Verify the final release"
57 | echo ${{ steps.finalize-release.outputs.release_url }}
58 |
59 | - name: "** Failure Message **"
60 | if: ${{ failure() }}
61 | run: |
62 | echo "Failed to Publish the Draft Release Url:"
63 | echo ${{ steps.populate-release.outputs.release_url }}
64 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | push:
4 | branches: ["main"]
5 | pull_request:
6 | schedule:
7 | - cron: "0 0 * * *"
8 |
9 | defaults:
10 | run:
11 | shell: bash -eux {0}
12 |
13 | env:
14 | PYTHONDONTWRITEBYTECODE: 1
15 |
16 | jobs:
17 | lint:
18 | name: Test Lint
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v4
22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
23 | - name: Run Linters
24 | run: |
25 | hatch run typing:test
26 | hatch run lint:build
27 | pipx run interrogate -v jupyter_releaser
28 | pipx run doc8 --max-line-length=200
29 |
30 | check_links:
31 | runs-on: ubuntu-latest
32 | steps:
33 | - uses: actions/checkout@v4
34 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
35 | - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
36 |
37 | test:
38 | runs-on: ${{ matrix.os }}
39 | strategy:
40 | fail-fast: false
41 | matrix:
42 | os: [ubuntu-latest, windows-latest, macos-latest]
43 | python-version: ["3.9", "3.13"]
44 | steps:
45 | - uses: actions/checkout@v4
46 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
47 |
48 | - name: Run the tests with coverage on Ubuntu
49 | if: ${{ matrix.os == 'ubuntu-latest' }}
50 | run: |
51 | hatch run cov:test -n auto --cov-fail-under 80 || hatch run test:test --lf
52 |
53 | - name: Run the tests on Windows and MacOS
54 | if: ${{ matrix.os != 'ubuntu-latest' }}
55 | run: hatch run cov:test -s -n auto || hatch run cov:test -s --lf
56 |
57 | - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1
58 |
59 | coverage:
60 | runs-on: ubuntu-latest
61 | needs:
62 | - test
63 | steps:
64 | - uses: actions/checkout@v4
65 | - uses: jupyterlab/maintainer-tools/.github/actions/report-coverage@v1
66 |
67 | generate_changelog:
68 | runs-on: ubuntu-latest
69 | timeout-minutes: 10
70 | steps:
71 | - uses: actions/checkout@v4
72 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
73 | - env:
74 | GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75 | RH_REPOSITORY: jupyter-server/jupyter_releaser
76 | RH_SINCE: v0.10.2
77 | RH_UNTIL: v0.10.3
78 | run: |
79 | set -eux
80 | pip install -e .
81 | python -m jupyter_releaser.actions.generate_changelog
82 | cat CHANGELOG_ENTRY.md
83 | # Check for version entry contents
84 | cat CHANGELOG_ENTRY.md | grep -q "#234"
85 | cat CHANGELOG_ENTRY.md | grep -q "compare/${RH_SINCE}...${RH_UNTIL}"
86 | # make sure it works with different settings
87 | export RH_SINCE=
88 | export RH_UNTIL=
89 | export RH_CONVERT_TO_RST=true
90 | sudo apt-get install pandoc
91 | pip install pypandoc
92 | python -m jupyter_releaser.actions.generate_changelog
93 | cat CHANGELOG_ENTRY.md
94 |
95 | test_minimum_versions:
96 | name: Test Minimum Versions
97 | timeout-minutes: 20
98 | runs-on: ubuntu-latest
99 | steps:
100 | - uses: actions/checkout@v4
101 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
102 | with:
103 | dependency_type: minimum
104 | - name: Run the unit tests
105 | run: |
106 | hatch run test:nowarn || hatch run test:nowarn --lf
107 |
108 | test_prereleases:
109 | name: Test Prereleases
110 | runs-on: ubuntu-latest
111 | timeout-minutes: 20
112 | steps:
113 | - uses: actions/checkout@v4
114 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
115 | with:
116 | dependency_type: pre
117 | python_version: "3.12"
118 | - name: Run the tests
119 | run: |
120 | hatch run test:nowarn || hatch run test:nowarn --lf
121 |
122 | docs:
123 | runs-on: ubuntu-latest
124 | timeout-minutes: 10
125 | steps:
126 | - uses: actions/checkout@v4
127 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
128 | - run: hatch run docs:build
129 |
130 | check_local_actions:
131 | runs-on: ubuntu-latest
132 | steps:
133 | - uses: actions/checkout@v4
134 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
135 |
136 | - name: prep-release
137 | uses: ./.github/actions/prep-release
138 | with:
139 | token: ${{ secrets.GITHUB_TOKEN }}
140 | dry_run: true
141 |
142 | - name: populate-release
143 | uses: ./.github/actions/populate-release
144 | with:
145 | token: ${{ secrets.GITHUB_TOKEN }}
146 | release_url: ${{ steps.prep-release.outputs.release_url }}
147 | dry_run: true
148 |
149 | - name: publish-release
150 | uses: ./.github/actions/finalize-release
151 | with:
152 | token: ${{ secrets.GITHUB_TOKEN }}
153 | release_url: ${{ steps.populate-release.outputs.release_url }}
154 | dry_run: true
155 |
156 | check: # This job does nothing and is only used for the branch protection
157 | if: always()
158 | needs:
159 | - check_links
160 | - coverage
161 | - docs
162 | - lint
163 | - check_local_actions
164 | - test_minimum_versions
165 | # disabled until we can use pypiserver 2.3.x
166 | # which supports Python 3.13, see
167 | # https://github.com/pypiserver/pypiserver/issues/630
168 | # - test_prereleases
169 | - generate_changelog
170 | runs-on: ubuntu-latest
171 | steps:
172 | - name: Decide whether the needed jobs succeeded or failed
173 | uses: re-actors/alls-green@release/v1
174 | with:
175 | jobs: ${{ toJSON(needs) }}
176 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # Local git checkout
132 | .jupyter_releaser_checkout
133 |
134 | # macOS
135 | .DS_Store
136 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autoupdate_schedule: monthly
3 | autoupdate_commit_msg: "chore: update pre-commit hooks"
4 |
5 | repos:
6 | - repo: https://github.com/pre-commit/pre-commit-hooks
7 | rev: v4.5.0
8 | hooks:
9 | - id: check-case-conflict
10 | - id: check-ast
11 | - id: check-docstring-first
12 | - id: check-executables-have-shebangs
13 | - id: check-added-large-files
14 | - id: check-case-conflict
15 | - id: check-merge-conflict
16 | - id: check-json
17 | - id: check-toml
18 | - id: check-yaml
19 | - id: debug-statements
20 | - id: end-of-file-fixer
21 | - id: trailing-whitespace
22 |
23 | - repo: https://github.com/python-jsonschema/check-jsonschema
24 | rev: 0.27.4
25 | hooks:
26 | - id: check-github-workflows
27 |
28 | - repo: https://github.com/executablebooks/mdformat
29 | rev: 0.7.17
30 | hooks:
31 | - id: mdformat
32 | additional_dependencies:
33 | [mdformat-gfm, mdformat-frontmatter, mdformat-footnote]
34 |
35 | - repo: https://github.com/pre-commit/mirrors-prettier
36 | rev: "v4.0.0-alpha.8"
37 | hooks:
38 | - id: prettier
39 | types_or: [yaml, html, json]
40 | exclude: '.pre-commit-config.yaml'
41 |
42 | - repo: https://github.com/pre-commit/mirrors-mypy
43 | rev: "v1.8.0"
44 | hooks:
45 | - id: mypy
46 | files: "^jupyter_releaser"
47 | exclude: "tests"
48 | stages: [manual]
49 | args: ["--install-types", "--non-interactive"]
50 | additional_dependencies:
51 | ["toml", "requests", "ghapi", "packaging","pkginfo", "fastapi", "click", "github_activity", "mdformat"]
52 |
53 | - repo: https://github.com/adamchainz/blacken-docs
54 | rev: "1.16.0"
55 | hooks:
56 | - id: blacken-docs
57 | additional_dependencies: [black==23.7.0]
58 |
59 | - repo: https://github.com/codespell-project/codespell
60 | rev: "v2.2.6"
61 | hooks:
62 | - id: codespell
63 | args: ["-L", "sur,nd"]
64 |
65 | - repo: https://github.com/pre-commit/pygrep-hooks
66 | rev: "v1.10.0"
67 | hooks:
68 | - id: rst-backticks
69 | - id: rst-directive-colons
70 | - id: rst-inline-touching-normal
71 |
72 | - repo: https://github.com/astral-sh/ruff-pre-commit
73 | rev: v0.2.0
74 | hooks:
75 | - id: ruff
76 | args: ["--fix", "--show-fixes"]
77 | - id: ruff-format
78 |
79 | - repo: https://github.com/scientific-python/cookie
80 | rev: "2024.01.24"
81 | hooks:
82 | - id: sp-repo-review
83 | additional_dependencies: ["repo-review[cli]"]
84 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | sphinx:
3 | configuration: docs/source/conf.py
4 | python:
5 | install:
6 | # install itself with pip install .
7 | - method: pip
8 | path: .
9 | extra_requirements:
10 | - docs
11 | build:
12 | os: ubuntu-22.04
13 | tools:
14 | python: "3.11"
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Jupyter Releaser
2 |
3 | ## General Jupyter contributor guidelines
4 |
5 | If you're reading this section, you're probably interested in contributing to
6 | Jupyter. Welcome and thanks for your interest in contributing!
7 |
8 | Please take a look at the Contributor documentation, familiarize yourself with
9 | using the Jupyter Server, and introduce yourself on the mailing list and
10 | share what area of the project you are interested in working on.
11 |
12 | For general documentation about contributing to Jupyter projects, see the
13 | [Project Jupyter Contributor Documentation](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html)
14 |
15 | ## Setting Up a Development Environment
16 |
17 | Use the following steps:
18 |
19 | ```bash
20 | python -m pip install --upgrade setuptools pip
21 | git clone https://github.com/jupyter-server/jupyter_releaser
22 | cd jupyter-releaser
23 | pip install -e .[test]
24 | ```
25 |
26 | If you are using a system-wide Python installation and you only want to install the package for you,
27 | you can add `--user` to the install commands.
28 |
29 | Set up pre-commit hooks for automatic code formatting, etc.
30 |
31 | ```bash
32 | pre-commit install
33 | ```
34 |
35 | You can also invoke the pre-commit hook manually at any time with
36 |
37 | ```bash
38 | pre-commit run --all-files --hook-stage manual
39 | ```
40 |
41 | Once you have done this, you can launch the main branch of Jupyter Releaser
42 | from any directory in your system with::
43 |
44 | ```bash
45 | jupyter-releaser --help
46 | ```
47 |
48 | ## Running Tests
49 |
50 | To run the Python tests, use:
51 |
52 | ```bash
53 | hatch run test:test
54 | ```
55 |
56 | ## Documentation
57 |
58 | Contributions can also take the form of fixes and improvements to the documentation.
59 |
60 | To build the docs, run:
61 |
62 | ```bash
63 | hatch run docs:build
64 | ```
65 |
66 | To serve the docs:
67 |
68 | ```bash
69 | hatch run docs:serve
70 | ```
71 |
72 | It is also possible to automatically watch the docs with the following command:
73 |
74 | ```bash
75 | hatch run docs:watch
76 | ```
77 |
78 | Then open http://localhost:8000 in your browser to view the documentation.
79 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2021 Project Jupyter Contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of the copyright holder nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
30 | Tee File License
31 | ================
32 |
33 | The tee.py file is from https://github.com/pycontribs/subprocess-tee/
34 | which is licensed under the "MIT" license. See the tee.py file for details.
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jupyter Releaser
2 |
3 | [](https://github.com/jupyter-server/jupyter_releaser/actions/workflows/test.yml/badge.svg?query=branch%3Amain++)
4 | [](http://jupyter-releaser.readthedocs.io/en/latest/?badge=latest)
5 |
6 | **Jupyter Releaser** contains a set of helper scripts and GitHub Actions to aid in automated releases of Python and npm packages.
7 |
8 | ## Installation
9 |
10 | To install the latest release locally, make sure you have
11 | [pip installed](https://pip.readthedocs.io/en/stable/installing/) and run:
12 |
13 | ```bash
14 | pip install git+https://github.com/jupyter-server/jupyter_releaser
15 | ```
16 |
17 | ## Library Usage
18 |
19 | ```bash
20 | jupyter-releaser --help
21 | jupyter-releaser build-python --help
22 | jupyter-releaser check-npm --help
23 | ```
24 |
25 | ## Checklist for Adoption
26 |
27 | See the [adoption guides](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/index.html).
28 |
29 | ## Actions
30 |
31 | GitHub actions scripts are available to draft a changelog, draft a release, publish a release, and check a release.
32 |
33 | See the [action details documentation](https://jupyter-releaser.readthedocs.io/en/latest/reference/theory.html#action-details) for more information.
34 |
35 | The actions can be run on a [fork](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_releaser.html) of `jupyter_releaser` and target multiple
36 | repositories, or run as workflows on the [source repositories](https://jupyter-releaser.readthedocs.io/en/latest/how_to_guides/convert_repo_from_repo.html), using
37 | shared credentials.
38 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release Process
2 |
3 | - Run through the "Draft Changelog", "Draft Release", and "Publish Release" workflows
4 | - Update the tag for GitHub Actions consumers, e.g.
5 |
6 | ```bash
7 | git fetch origin main
8 | git checkout main
9 | git tag -f v1
10 | git push -f origin --tags
11 | ```
12 |
--------------------------------------------------------------------------------
/docs/source/_static/custom.css:
--------------------------------------------------------------------------------
1 | /* Added to avoid logo being too squeezed */
2 | .navbar-brand {
3 | height: 4rem !important;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/source/_static/images/logo/favicon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/docs/source/_static/images/logo/logo.svg:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 | # -- Path setup --------------------------------------------------------------
7 | # If extensions (or modules to document with autodoc) are in another directory,
8 | # add these directories to sys.path here. If the directory is relative to the
9 | # documentation root, use os.path.abspath to make it absolute, like shown here.
10 | #
11 | # import os
12 | # import sys
13 | # sys.path.insert(0, os.path.abspath('.'))
14 | import importlib.metadata
15 | import os.path as osp
16 |
17 | HERE = osp.abspath(osp.dirname(__file__))
18 |
19 | # -- Project information -----------------------------------------------------
20 |
21 | project = "Jupyter Releaser"
22 | copyright = "2024, Project Jupyter"
23 | author = "Project Jupyter"
24 |
25 | # The full version, including alpha/beta/rc tags.
26 | release = importlib.metadata.version("jupyter_releaser")
27 | # The short X.Y version.
28 | version = ".".join(release.split(".")[:2])
29 |
30 | # -- General configuration ---------------------------------------------------
31 |
32 | # Add any Sphinx extension module names here, as strings. They can be
33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
34 | # ones.
35 | extensions = [
36 | "sphinx.ext.autodoc",
37 | "sphinx.ext.intersphinx",
38 | "sphinx.ext.napoleon",
39 | "myst_parser",
40 | "sphinx_click",
41 | ]
42 |
43 | try:
44 | import enchant # noqa: F401
45 |
46 | extensions += ["sphinxcontrib.spelling"]
47 | except ImportError:
48 | pass
49 |
50 | myst_enable_extensions = ["html_image", "tasklist"]
51 |
52 | # Add any paths that contain templates here, relative to this directory.
53 | templates_path = ["_templates"]
54 |
55 | # List of patterns, relative to source directory, that match files and
56 | # directories to ignore when looking for source files.
57 | # This pattern also affects html_static_path and html_extra_path.
58 | # exclude_patterns = []
59 |
60 |
61 | # -- Options for HTML output -------------------------------------------------
62 |
63 | # The theme to use for HTML and HTML Help pages. See the documentation for
64 | # a list of builtin themes.
65 | #
66 | html_theme = "pydata_sphinx_theme"
67 |
68 | html_logo = "_static/images/logo/logo.svg"
69 | html_favicon = "_static/images/logo/favicon.svg"
70 |
71 | # Add any paths that contain custom static files (such as style sheets) here,
72 | # relative to this directory. They are copied after the builtin static files,
73 | # so a file named "default.css" will overwrite the builtin "default.css".
74 | html_static_path = ["_static"]
75 |
76 | # Add an Edit this Page button
77 | html_theme_options = {
78 | "icon_links": [
79 | {
80 | "name": "GitHub",
81 | "url": "https://github.com/jupyter-server/jupyter_releaser",
82 | "icon": "fab fa-github-square",
83 | }
84 | ],
85 | "use_edit_page_button": True,
86 | "navigation_with_keys": False,
87 | }
88 |
89 | # Output for github to be used in links
90 | html_context = {
91 | "github_user": "jupyter-server", # Username
92 | "github_repo": "jupyter_releaser", # Repo name
93 | "github_version": "main", # Version
94 | "doc_path": "/docs/source/", # Path in the checkout to the docs root
95 | }
96 |
97 |
98 | def setup(app):
99 | app.add_css_file("custom.css")
100 |
--------------------------------------------------------------------------------
/docs/source/faq/index.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ## How to resume a failed release?
4 |
5 | If the workflow failed to publish the release, for example because PyPI or npm was down during the release process, we can try to re-run the failed "Publish Release" workflow.
6 |
7 | If the draft GitHub release was correctly created, re-run the workflow this time specifying `populate-release` as a step to skip. That way the assets already attached to the draft GitHub Release and the associated release commit will not be recreated, and the workflow will skip to the "Finalize Release" step directly.
8 |
9 | ## Failed to publish a package to `npm`
10 |
11 | The releaser may fail to publish a package to the `npm` in the following cases:
12 |
13 | - `npmjs.com` is down, or `npm` is encountering issues publishing new packages
14 | - the account publishing the package to npm is not part of the list of collaborators
15 | - the package you are trying to publish does not contain the correct publish config. If the package is meant to be public, add the following to `package.json`:
16 |
17 | ```json
18 | "publishConfig": {
19 | "access": "public"
20 | },
21 | ```
22 |
23 | ## `check-python` step fails with Python monorepos
24 |
25 | If you develop multiple Python packages within the same repository (as a monorepo), and the Python packages depend on each other, for example:
26 |
27 | ```
28 | packages
29 | ├── bar
30 | └── foo
31 | ```
32 |
33 | And `bar` depends on `foo`, for example with `foo>=1.0.0`. You may see the following error during the `check-python` step:
34 |
35 | ```
36 | ERROR: Could not find a version that satisfies the requirement foo>=1.0.0 (from bar) (from versions: 1.0.0b4, 1.0.0b5, 1.0.0b6, 1.0.0b8)
37 | ERROR: No matching distribution found for foo>=1.0.0
38 | ```
39 |
40 | This issue is not fixed yet and is being tracked in [this issue](https://github.com/jupyter-server/jupyter_releaser/issues/499).
41 |
42 | As a workaround, you can skip the `check-python` step with the following releaser config:
43 |
44 | ```toml
45 | [tool.jupyter-releaser]
46 | skip = ["check-python"]
47 | ```
48 |
49 | ## How to only publish to `npm`?
50 |
51 | If you would like to use the Jupyter Releaser to publish to `npm` only, you can configure the releaser to skip the `build-python` step:
52 |
53 | ```toml
54 | [tool.jupyter-releaser]
55 | skip = ["build-python"]
56 | ```
57 |
58 | ## My changelog is out of sync
59 |
60 | Create a new manual PR to fix the PR and re-orient the changelog entry markers.
61 |
62 | ## PR is merged to the target branch in the middle of a "Draft Release"
63 |
64 | The release will fail to push commits because it will not be up to date. Delete the pushed tags and re-start with "Draft Changelog" to
65 | pick up the new PR.
66 |
67 | ## What happens if one of my steps is failing but I want to force a release?
68 |
69 | This could happen for example if you need to override PRs to include in the changelog. In that case you would pass "check-changelog" to the
70 | workflow's "steps_to_skip" input option.
71 |
--------------------------------------------------------------------------------
/docs/source/get_started/generate_changelog.md:
--------------------------------------------------------------------------------
1 | # Generating a Manual Changelog Entry
2 |
3 | The "Generate Changelog" workflow is useful for repositories or branches that do not yet use Jupyter Releaser.
4 |
5 | You can generate a markdown or reStructuredText changelog entry and manually apply it to the repo.
6 |
7 | The workflow does not require any credentials.
8 |
9 | To run the workflow:
10 |
11 | - Go to the "Actions" tab on the source repository or your fork.
12 |
13 | - Click on "Generate Changelog" on the left navigation bar
14 |
15 | - Click the "Run workflow" dropdown button on the right and fill in the appropriate values
16 |
17 | 
18 |
19 | - Write "true" for "Whether to convert to RST" if the target repo uses a reStructuredText changelog
20 |
21 | - The last two fields can be used to constrain the changelog entries to a set of tags or other references
22 |
23 | - When the job finishes, download and extract the generated changelog
24 |
--------------------------------------------------------------------------------
/docs/source/get_started/index.md:
--------------------------------------------------------------------------------
1 | # Getting Started
2 |
3 | Tutorials. A hands-on introduction to Jupyter Releaser for maintainers.
4 |
5 | ```{toctree}
6 | :caption: 'Contents:'
7 | :maxdepth: 1
8 |
9 | making_release_from_repo
10 | making_release_from_releaser
11 | generate_changelog
12 | ```
13 |
--------------------------------------------------------------------------------
/docs/source/get_started/making_release_from_releaser.md:
--------------------------------------------------------------------------------
1 | # Making Your First Release from Jupyter Releaser
2 |
3 | This guide covers creating your first release on a repository that
4 | already uses Jupyter Releaser.
5 |
6 | ## Prerequisites
7 |
8 | - Write access to the target repository
9 | - Publish access to PYPI and/or npm assets associated with the repo
10 |
11 | ## Set up
12 |
13 | - Fork `jupyter_releaser`
14 |
15 | - Generate a [GitHub Access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with access to target GitHub repo to run GitHub Actions
16 |
17 | - Add the token as `ADMIN_GITHUB_TOKEN` in the [repository secrets](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository) of your fork. The token must have `repo` and `workflow` scopes.
18 |
19 | - Set up PyPI:
20 |
21 | Using PyPI token (legacy way)
22 |
23 | - 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.
24 |
25 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`.
26 |
27 | - 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:
28 |
29 | ```text
30 | owner1/repo1,token1
31 | owner2/repo2,token2
32 | ```
33 |
34 | If you have multiple Python packages in the same repository, you can point to them as follows:
35 |
36 | ```text
37 | owner1/repo1/path/to/package1,token1
38 | owner1/repo1/path/to/package2,token2
39 | ```
40 |
41 |
42 |
43 | Using PyPI trusted publisher (modern way)
44 |
45 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
46 | - if you use the example workflows, the _workflow name_ is `publish-release.yml` (or `full-release.yml`) and the
47 | _environment_ should be left blank.
48 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/))
49 |
50 |
51 |
52 | - If the repo generates npm release(s), add access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens), saved as `NPM_TOKEN` in "Secrets".
53 |
54 | > If you want to set _provenance_ on your package, you need to ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions)).
55 |
56 | ## Prep Release
57 |
58 | - Go to the "Actions" tab in your fork of `jupyter_releaser`
59 |
60 | - Select the "Step 1: Prep Release" workflow on the left
61 |
62 | - Click on the "Run workflow" dropdown button on the right
63 |
64 | - Fill in the appropriate parameters
65 |
66 | 
67 |
68 | - The "New Version Spec" will usually be the full version (e.g. 0.7.1). Repos using `tbump` can also use the "next" or "patch"
69 | option, which will bump the micro version (or the build version in the case of a prerelease). The "minor" option allows projects using "tbump" to bump
70 | to the next minor version directly. Note: The "next" and "patch" options
71 | are not available when using dev versions, you must use explicit versions
72 | instead.
73 |
74 | - Use the "since" field to select PRs prior to the latest tag to include in the release
75 |
76 | - Check "Use PRs with activity since the last stable git tag" if you would like to include PRs since the last non-prerelease version tagged on the target repository and branch.
77 |
78 | - Check "Set a placeholder in the changelog and don't publish the release" if
79 | you want to carry a silent release (e.g. in case of a security release).
80 | That option will change the default behavior by keeping the version
81 | changelog only in the GitHub release and keeping it private (aka in _Draft_
82 | state). The changelog file will only contains a placeholder to be replaced
83 | by the release body once the maintainers have chosen to publish the release.
84 |
85 | - The additional "Post Version Spec" field should be used if your repo uses a dev version (e.g. 0.7.0.dev0)
86 |
87 | - The workflow will use the GitHub API to find the relevant pull requests and make an appropriate changelog entry.
88 |
89 | - The workflow will create a draft GitHub release to the target
90 | repository and branch, with the draft changelog contents.
91 |
92 | 
93 |
94 | ## Review Changelog
95 |
96 | - Go to the draft GitHub Release created by the "Prep Release" workflow
97 |
98 | 
99 |
100 | - Review the contents, fixing typos or otherwise editing as necessary.
101 |
102 | - If there is a section called "Other Merged PRs", it means those PRs did not have one of the appropriate labels. If desired, you can go label those PRs and then re-run the workflow, or move the entries manually to the desired section. The appropriate labels are: bug, maintenance, enhancement, feature, and documentation.
103 |
104 | ## Publish Release
105 |
106 | - Return to your fork of `jupyter_releaser`
107 |
108 | - Click on the "Actions" tab
109 |
110 | - Select the "Publish Release" workflow on the left
111 |
112 | - Click on the "Run workflow" button on the right
113 |
114 | - Fill in the target repository
115 |
116 | - (Optional) Fill in draft GitHub Release URL given by the Changelog PR.
117 | If you leave it blank it will use the most recent draft GitHub release.
118 |
119 | 
120 |
121 | - The workflow will finish the GitHub release and publish assets to the appropriate registries.
122 |
123 | - If the workflow is not targeting the default branch, it will also generate a forward-port pull request for the changelog entry to the default branch.
124 |
125 | - When the workflow finishes it will print a link to the GitHub release and the forward-port PR (if appropriate) in the "\*\* Next Step \*\*" output.
126 |
127 | 
128 |
129 | - **Note** If the publish portion fails you can attempt to publish the draft GitHub release given by the URL in the "\*\* Failure Message \*\*" using the "Publish Release" workflow again. It will skip past the asset creation phase
130 | and move into asset publish.
131 |
132 | - **Note** GitHub Actions caches the secrets used on a given workflow run. So if you run into an auth issue, you'll
133 | need to run a new workflow instead of re-running the existing workflow.
134 |
135 | - Review and merge the forward-port PR if applicable
136 |
137 | - Announce the release on appropriate channels
138 |
--------------------------------------------------------------------------------
/docs/source/get_started/making_release_from_repo.md:
--------------------------------------------------------------------------------
1 | # Making Your First Release from Repo
2 |
3 | This guide covers creating your first release on a repository that
4 | already uses Jupyter Releaser using workflows on its own repository.
5 |
6 | ## Prerequisites
7 |
8 | - Admin write access to the target repository
9 | - Completed the [Checklist for Adoption](../how_to_guides/convert_repo_from_repo.md)
10 |
11 | ## Prep Release
12 |
13 | - Go to the "Actions" tab in the repository.
14 |
15 | - Select the "Step 1: Prep Release" workflow on the left
16 |
17 | - Click on the "Run workflow" dropdown button on the right
18 |
19 | - Fill in the appropriate parameters
20 |
21 | 
22 |
23 | - The "New Version Spec" will usually be the full version (e.g. 0.7.1).
24 | Repos using `hatch` can also use [segments](https://hatch.pypa.io/latest/version/#supported-segments) such as _patch_, _minor_, _alpha_,... .
25 |
26 | - Use the "since" field to select PRs prior to the latest tag to include in the release
27 |
28 | - Check "Use PRs with activity since the last stable git tag" if you would like to include PRs since the last non-prerelease version tagged on the target repository and branch.
29 |
30 | - The additional "Post Version Spec" field should be used if your repo uses a dev version (e.g. 0.7.0.dev0)
31 |
32 | - Check "Set a placeholder in the changelog and don't publish the release" if
33 | you want to carry a silent release (e.g. in case of a security release).
34 | That option will change the default behavior by keeping the version
35 | changelog only in the GitHub release and keeping it private (aka in _Draft_
36 | state). The changelog file will only contains a placeholder to be replaced
37 | by the release body once the maintainers have chosen to publish the release.
38 |
39 | - The workflow will use the GitHub API to find the relevant pull requests and make an appropriate changelog entry.
40 |
41 | - The workflow will create a draft GitHub release to the target
42 | repository and branch, with the draft changelog contents.
43 |
44 | 
45 |
46 | ## Review Changelog
47 |
48 | - Go to the draft GitHub Release created by the "Prep Release" workflow
49 |
50 | 
51 |
52 | - Review the contents, fixing typos or otherwise editing as necessary.
53 |
54 | - If there is a section called "Other Merged PRs", it means those PRs did not have one of the appropriate labels. If desired, you can go label those PRs and then re-run the workflow, or move the entries manually to the desired section. The appropriate labels are: bug, maintenance, enhancement, feature, and documentation.
55 |
56 | ## Publish Release
57 |
58 | - Return to "Actions" tab for the repository
59 |
60 | - Select the "Publish Release" workflow on the left
61 |
62 | - Click on the "Run workflow" button on the right
63 |
64 | - (Optional) Fill in draft GitHub Release URL given by the Changelog PR.
65 | If you leave it blank it will use the most recent draft GitHub release.
66 |
67 | 
68 |
69 | - The workflow will finish the GitHub release and publish assets to the appropriate registries.
70 |
71 | - If the workflow is not targeting the default branch, it will also generate a forward-port pull request for the changelog entry to the default branch.
72 |
73 | - When the workflow finishes it will print a link to the GitHub release and the forward-port PR (if appropriate) in the "\*\* Next Step \*\*" output.
74 |
75 | 
76 |
77 | - **Note** If the publish portion fails you can attempt to publish the draft GitHub release given by the URL in the "\*\* Failure Message \*\*" using the "Publish Release" workflow again. It will skip past the asset creation phase
78 | and move into asset publish.
79 |
80 | - **Note** GitHub Actions caches the secrets used on a given workflow run. So if you run into an auth issue, you'll
81 | need to run a new workflow instead of re-running the existing workflow.
82 |
83 | - Review and merge the forward-port PR if applicable
84 |
85 | - Announce the release on appropriate channels
86 |
--------------------------------------------------------------------------------
/docs/source/how_to_guides/convert_repo_from_releaser.md:
--------------------------------------------------------------------------------
1 | # Convert a Repo to Use Releaser from Releaser
2 |
3 | Follow the steps below to convert a repository to use Jupyter Releaser for releases, where maintainers make releases from a fork of Jupyter Releaser.
4 |
5 | ## Prerequisites
6 |
7 | See checklist below for details:
8 |
9 | - Markdown changelog
10 | - Bump version configuration (if using Python), for example [tbump](https://github.com/dmerejkowsky/tbump)
11 | - [Access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with access to target GitHub repo to run GitHub Actions.
12 | - Access token for the [PyPI registry](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github)
13 | - If needed, access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens).
14 |
15 | ## Checklist for Adoption
16 |
17 | A. Prep the `jupyter_releaser` fork:
18 |
19 | - [ ] Clone this repository onto your GitHub user account.
20 |
21 | - [ ] Add a GitHub [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) with access to target GitHub repo to run
22 | GitHub Actions, saved as `ADMIN_GITHUB_TOKEN` in the
23 | [repository secrets](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository).
24 | The token will need "public_repo", and "repo:status" permissions.
25 |
26 | - [ ] Set up PyPI:
27 |
28 | Using PyPI token (legacy way)
29 |
30 | - Add access token for the [PyPI registry](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github) stored as `PYPI_TOKEN`.
31 | _Note_ For security reasons, it is recommended that you scope the access
32 | to a single repository, and use a variable called `PYPI_TOKEN_MAP` that is formatted as follows:
33 |
34 | ```text
35 | owner1/repo1,token1
36 | owner2/repo2,token2
37 | ```
38 |
39 | If you have multiple Python packages in the same repository, you can point to them as follows:
40 |
41 | ```text
42 | owner1/repo1/path/to/package1,token1
43 | owner1/repo1/path/to/package2,token2
44 | ```
45 |
46 |
47 |
48 | Using PyPI trusted publisher (modern way)
49 |
50 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
51 | - if you use the example workflows, the _workflow name_ is `publish-release.yml` (or `full-release.yml`) and the
52 | _environment_ should be left blank.
53 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/))
54 |
55 |
56 |
57 | - [ ] If needed, add access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens), saved as `NPM_TOKEN`.
58 |
59 | > If you want to set _provenance_ on your package, you need to ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions)).
60 |
61 | B. Prep target repository:
62 |
63 | - [ ] Switch to Markdown Changelog
64 | - We recommend [MyST](https://myst-parser.readthedocs.io/en/latest/?badge=latest), especially if some of your docs are in reStructuredText.
65 | - Can use `pandoc -s changelog.rst -o changelog.md` and some hand edits as needed.
66 | - Note that [directives](https://myst-parser.readthedocs.io/en/latest/using/syntax.html#syntax-directives) can still be used
67 | - [ ] Add HTML start and end comment markers to Changelog file - see example in [CHANGELOG.md](https://github.com/jupyter-server/jupyter_releaser/blob/main/CHANGELOG.md) (view in raw mode)
68 | - [ ] We recommend using [hatch](https://hatch.pypa.io/latest/) for your
69 | build system and for version handling.
70 | - If previously providing `version_info` like `version_info = (1, 7, 0, '.dev', '0')`, use a pattern like the one below in your version file:
71 |
72 | ```python
73 | import re
74 | from typing import List
75 |
76 | # Version string must appear intact for hatch versioning
77 | __version__ = "6.16.0"
78 |
79 | # Build up version_info tuple for backwards compatibility
80 | pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)"
81 | match = re.match(pattern, __version__)
82 | assert match is not None
83 | parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]]
84 | if match["rest"]:
85 | parts.append(match["rest"])
86 | version_info = tuple(parts)
87 | ```
88 |
89 | - If you need to keep node and python versions in sync, use [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version). See [nbformat](https://github.com/jupyter/nbformat/blob/main/pyproject.toml) for example.
90 |
91 | - [ ] Add a GitHub Actions CI step to run the `check_release` action. For example:
92 |
93 | ```yaml
94 | - name: Check Release
95 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
96 | with:
97 | token: ${{ secrets.GITHUB_TOKEN }}
98 | ```
99 |
100 | - This should be run on `push` and `pull` request events. You can copy
101 | the `check-release.yml` from this repo as an example.
102 |
103 | - [ ] If you would like the release assets to be uploaded as artifacts, add the following step after the `check_release` action:
104 |
105 | ```yaml
106 | - name: Upload Distributions
107 | uses: actions/upload-artifact@v2
108 | with:
109 | name: jupyter-releaser-dist-${{ github.run_number }}
110 | path: .jupyter_releaser_checkout/dist
111 | ```
112 |
113 | - [ ] Add a workflow that uses the [`enforce-label`](https://github.com/jupyterlab/maintainer-tools#enforce-labels) action from `jupyterlab/maintainer-tools` to ensure that all PRs have on of the triage labels used to
114 | categorize the changelog.
115 |
116 | - [ ] Update or add `RELEASE.md` that describes the onboarding and release process, e.g. [jupyter_server](https://github.com/jupyter-server/jupyter_server/blob/main/RELEASE.md).
117 |
118 | ## Release Workflow
119 |
120 | - [ ] Set up a fork of `jupyter-releaser` if you have not yet done so.
121 | - [ ] Run through the release process, targeting this repo and the appropriate branch
122 | - [ ] Optionally add configuration to the target repository if non-standard options or hooks are needed.
123 | - [ ] If desired, add `check_release` job, changelog, and `tbump` support to other active release branches
124 | - [ ] Try out the `Draft Changelog` and `Draft Release` process against a fork of the target repo first so you don't accidentally push tags and GitHub releases to the source repository.
125 | - [ ] Try the `Publish Release` process using a prerelease version before publishing a final version.
126 |
127 | ## Backport Branches
128 |
129 | - Create backport branches the usual way, e.g. `git checkout -b 3.0.x v3.0.1; git push origin 3.0.x`
130 | - When running the `Publish Release` Workflow, an automatic PR is generated for the default branch
131 | in the target repo, positioned in the appropriate place in the changelog.
132 |
--------------------------------------------------------------------------------
/docs/source/how_to_guides/convert_repo_from_repo.md:
--------------------------------------------------------------------------------
1 | # Convert a Repo to Use Releaser from Repo
2 |
3 | Follow the steps below to convert a repository to use Jupyter Releaser for releases, where maintainers make releases from the repository itself.
4 |
5 | ## Prerequisites
6 |
7 | See checklist below for details:
8 |
9 | - Markdown changelog
10 | - Bump version configuration (if using Python), for example [hatch](https://hatch.pypa.io/latest/)
11 | - [Add a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) to your PyPI project
12 | - If needed, access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens).
13 |
14 | ## Checklist for Adoption
15 |
16 | - [ ] Set up a [GitHub App](https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps#github-apps-that-act-on-their-own-behalf) on your organization (or personal account for a personal project).
17 |
18 | - Disable the web hook
19 | - Enable Repository permissions > Contents > Read and write
20 | - Select "Only on this account"
21 | - Click "Create GitHub App"
22 | - Browse to the App Settings
23 | - Select "Install App" and install on all repositories
24 | - Under "General" click "Generate a private key"
25 | - Store the `APP_ID` and the private key in a secure location (Jupyter Vault if using a Jupyter Org)
26 |
27 | - [ ] Create a "release" [environment](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment) on your repository and add an `APP_ID` Environment Variable and `APP_PRIVATE_KEY` secret.
28 | The environment should be enabled for ["Protected branches only"](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-branches-and-tags).
29 |
30 | - [ ] Configure [Rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets) for the repository
31 |
32 | - Set up branch protection (with default rules) on publication branches
33 | - Remove global tag protection.
34 | - Add a branch Ruleset for all branches
35 | - Allow the GitHub App to bypass protections
36 | - Set up Pull Request and Required Checks
37 | - Add a tags Ruleset for all tags
38 | - Allow the GitHub App to bypass protections
39 |
40 | - [ ] Copy `prep-release.yml` and `publish-release.yml` (or only `full-release.yml`) from the
41 | [example-workflows](https://github.com/jupyter-server/jupyter_releaser/tree/main/example-workflows) folder in this repository.
42 |
43 | - [ ] Set up PyPI:
44 |
45 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)
46 | - if you use the example workflows, the _workflow name_ is `publish-release.yml` (or `full-release.yml`) and the
47 | _environment_ should be `release` (the name of the GitHub environment).
48 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/))
49 |
50 | - [ ] If needed, add access token for [npm](https://docs.npmjs.com/creating-and-viewing-access-tokens), saved as `NPM_TOKEN`. Again this should
51 | be created using a machine account that only has publish access.
52 |
53 | - [ ] Ensure that only trusted users with 2FA have admin access to the repository, since they will be able to trigger releases.
54 |
55 | - [ ] Switch to Markdown Changelog
56 |
57 | - We recommend [MyST](https://myst-parser.readthedocs.io/en/latest/?badge=latest), especially if some of your docs are in reStructuredText.
58 | - Can use `pandoc -s changelog.rst -o changelog.md` and some hand edits as needed.
59 | - Note that [directives](https://myst-parser.readthedocs.io/en/latest/using/syntax.html#syntax-directives) can still be used
60 |
61 | - [ ] Add HTML start and end comment markers to Changelog file
62 |
63 | - see example in [CHANGELOG.md](https://github.com/jupyter-server/jupyter_releaser/blob/main/CHANGELOG.md) (view in raw mode)
64 |
65 | ```md
66 | # Changelog
67 |
68 |
69 |
70 |
71 | ```
72 |
73 | - [ ] We recommend using [hatch](https://hatch.pypa.io/latest/) for your
74 | build system and for version handling.
75 | - If previously providing `version_info` like `version_info = (1, 7, 0, '.dev', '0')`,
76 | use a pattern like the one below in your version file:
77 |
78 | ```python
79 | import re
80 | from typing import List
81 |
82 | # Version string must appear intact for hatch versioning
83 | __version__ = "6.16.0"
84 |
85 | # Build up version_info tuple for backwards compatibility
86 | pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)"
87 | match = re.match(pattern, __version__)
88 | assert match is not None
89 | parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]]
90 | if match["rest"]:
91 | parts.append(match["rest"])
92 | version_info = tuple(parts)
93 | ```
94 |
95 | - If you need to keep node and python versions in sync, use [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version).
96 |
97 | - See [nbformat](https://github.com/jupyter/nbformat/blob/main/pyproject.toml) for example.
98 |
99 | - [ ] Add a GitHub Actions CI step to run the `check_release` action. For example:
100 |
101 | - This should be run on `push` and `pull` request events. You can copy
102 | the `check-release.yml` from this repo as an example.
103 |
104 | ```yaml
105 | - name: Check Release
106 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
107 | with:
108 | token: ${{ secrets.GITHUB_TOKEN }}
109 | ```
110 |
111 | - [ ] If you would like the release assets to be uploaded as artifacts, add the following step after the `check_release` action:
112 |
113 | ```yaml
114 | - name: Upload Distributions
115 | uses: actions/upload-artifact@v4
116 | with:
117 | name: dist-${{ github.run_number }}
118 | path: .jupyter_releaser_checkout/dist
119 | ```
120 |
121 | - [ ] Add a workflow that uses the [`enforce-label`](https://github.com/jupyterlab/maintainer-tools#enforce-labels) action
122 | from `jupyterlab/maintainer-tools` to ensure that all PRs have on of the triage labels used to categorize the changelog.
123 |
124 | ```yaml
125 | name: Enforce PR label
126 |
127 | on:
128 | pull_request:
129 | types: [labeled, unlabeled, opened, edited, synchronize]
130 |
131 | jobs:
132 | enforce-label:
133 | runs-on: ubuntu-latest
134 | permissions:
135 | pull-requests: write
136 | steps:
137 | - name: enforce-triage-label
138 | uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1
139 | ```
140 |
141 | - [ ] Update or add `RELEASE.md` that describes the onboarding and release process, e.g. [jupyter_server](https://github.com/jupyter-server/jupyter_server/blob/main/RELEASE.md).
142 |
143 | - [ ] Optionally add configuration to the repository if non-standard options or hooks are needed.
144 |
145 | - [ ] If desired, add `check_release` job, changelog, and `hatch` support to other active release branches
146 |
147 | ## Initial Release Workflow
148 |
149 | - [ ] Try out the `Prep Release` and `Publish Release` process against a fork of the target repo first so you don't accidentally push tags and GitHub releases to the source repository. Set the `TWINE_REPOSITORY_URL` environment variable to `https://test.pypi.org/legacy/` in the "Finalize Release" action part of the workflow
150 |
151 | - [ ] Try the `Publish Release` process using a prerelease version on the main
152 | repository before publishing a final version.
153 |
154 | ## Backport Branches
155 |
156 | - Create backport branches the usual way, e.g. `git checkout -b 3.0.x v3.0.1; git push origin 3.0.x`
157 | - When running the `Publish Release` Workflow, an automatic PR is generated for the default branch
158 | in the target repo, positioned in the appropriate place in the changelog.
159 |
--------------------------------------------------------------------------------
/docs/source/how_to_guides/index.md:
--------------------------------------------------------------------------------
1 | # How-to Guides
2 |
3 | Step-by-step guides. Covers key tasks and operations and common problems
4 |
5 | ```{toctree}
6 | :caption: 'Contents:'
7 | :maxdepth: 1
8 |
9 | convert_repo_from_releaser
10 | convert_repo_from_repo
11 | write_config
12 | maintain_fork
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/source/how_to_guides/maintain_fork.md:
--------------------------------------------------------------------------------
1 | # Maintain a Releaser Fork
2 |
3 | ## How to keep fork of Jupyter Releaser up to date
4 |
5 | - The manual workflow files target the `@v2` actions in the source repository, which means that as long as
6 | the workflow files themselves are up to date, you will always be running the most up to date actions.
7 |
8 | - Make sure your workflow is up to date by checking the "Fetch Upstream" dropdown on the main page of your fork.
9 |
10 | 
11 |
--------------------------------------------------------------------------------
/docs/source/how_to_guides/write_config.md:
--------------------------------------------------------------------------------
1 | # Write Releaser Config
2 |
3 | ## Command Options and Environment Variables
4 |
5 | All of the commands support CLI and Environment Variable Overrides.
6 | The environment variables are defined by the `envvar` parameters in the
7 | command options in `cli.py`. The environment variables unique to
8 | `jupyter-releaser` are prefixed with `RH_`. A list of all env variables can be seen
9 | by running `jupyter-releaser list-envvars`.
10 |
11 | ## Default Values, Options, Skip, and Hooks
12 |
13 | The default values can also be overridden using a config file.
14 |
15 | Options can be overridden using the `options` section.
16 |
17 | You can skip one or more commands using a `skip` section, which is a list of
18 | commands to skip.
19 |
20 | You can also define hooks to run before and after
21 | commands in a `hooks` section. Hooks can be a shell command to run or
22 | a list of shell commands, and are specified to run `before-` or `after-`
23 | a command.
24 | Note: the only unusable hook names are `before-prep-git` and `before-extract-release`, since a checkout of the target repository is not yet available at that point.
25 |
26 | ## Configuration File Priority
27 |
28 | This is where `jupyter-releaser` looks for configuration (first one found is used):
29 |
30 | - `.jupyter-releaser.toml`
31 | - `pyproject.toml` (in the tool.jupyter-releaser section)
32 | - `package.json` (in the jupyter-releaser property)
33 |
34 | Example `.jupyter-releaser.toml`:
35 |
36 | ```toml
37 | [options]
38 | dist_dir = "mydist"
39 |
40 | [hooks]
41 | before-tag-version = "npm run pre:tag:script"
42 | ```
43 |
44 | Example `pyproject.toml` section:
45 |
46 | ```toml
47 | [tool.jupyter-releaser.options]
48 | dist_dir = "mydist"
49 |
50 | [tool.jupyter-releaser]
51 |
52 | [tool.jupyter-releaser.hooks]
53 | after-build-python = ["python scripts/cleanup.py", "python scripts/send_email.py"]
54 | ```
55 |
56 | Example `package.json`:
57 |
58 | ```json
59 | {
60 | "name": "my-package",
61 | "jupyter-releaser": {
62 | "options": {
63 | "dist_dir": "mydist"
64 | },
65 | "skip": ["check-npm"],
66 | "hooks": {
67 | "before-publish-dist": "npm run pre:publish:dist"
68 | }
69 | }
70 | }
71 | ```
72 |
73 | ## Automatic Dev Versions
74 |
75 | If you'd like to use dev versions for your repository between builds,
76 | use `dev` as the `post-version-spec` setting, e.g.
77 |
78 | ```toml
79 | [tool.jupyter-releaser.options]
80 | post-version-spec = "dev"
81 | ```
82 |
83 | This will bump it to the next minor release with a `.dev0` suffix.
84 |
85 | ## Ensuring Python Resource Files
86 |
87 | If you want to ensure that resource files are included in your installed Python
88 | package
89 | (from an sdist or a wheel), include configuration like the following:
90 |
91 | ```toml
92 | [tool.jupyter-releaser.options]
93 | pydist_resource_paths = ["my-package/img1.png", "my-package/foo/bar.json"]
94 | ```
95 |
--------------------------------------------------------------------------------
/docs/source/images/draft_github_release.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/draft_github_release.png
--------------------------------------------------------------------------------
/docs/source/images/final_github_release.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/final_github_release.png
--------------------------------------------------------------------------------
/docs/source/images/fork_fetch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/fork_fetch.png
--------------------------------------------------------------------------------
/docs/source/images/generate_changelog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/generate_changelog.png
--------------------------------------------------------------------------------
/docs/source/images/prep_release.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/prep_release.png
--------------------------------------------------------------------------------
/docs/source/images/prep_release_next_step.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/prep_release_next_step.png
--------------------------------------------------------------------------------
/docs/source/images/prep_release_repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/prep_release_repo.png
--------------------------------------------------------------------------------
/docs/source/images/publish_release.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/publish_release.png
--------------------------------------------------------------------------------
/docs/source/images/publish_release_next_step.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/publish_release_next_step.png
--------------------------------------------------------------------------------
/docs/source/images/publish_release_repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/docs/source/images/publish_release_repo.png
--------------------------------------------------------------------------------
/docs/source/index.md:
--------------------------------------------------------------------------------
1 | # Jupyter Releaser
2 |
3 | **Jupyter Releaser** contains a set of helper scripts and GitHub Actions to aid in automated releases of Python and npm packages.
4 |
5 | ## Documentation
6 |
7 | ```{toctree}
8 | :maxdepth: 2
9 |
10 | get_started/index
11 | how_to_guides/index
12 | reference/index
13 | faq/index
14 | ```
15 |
16 | ```{toctree}
17 | :maxdepth: 1
18 |
19 | reference/changelog
20 | ```
21 |
22 | ### Blog post
23 |
24 | For a high level overview of the Jupyter Releaser, check out [this post](https://blog.jupyter.org/automate-your-releases-with-the-jupyter-releaser-701e7b9841e6) on the Jupyter blog (published on October 29th, 2024).
25 |
26 | ## Indices and tables
27 |
28 | - {ref}`genindex`
29 | - {ref}`modindex`
30 | - {ref}`search`
31 |
--------------------------------------------------------------------------------
/docs/source/reference/api_docs.md:
--------------------------------------------------------------------------------
1 | # API Docs
2 |
3 | ## Library Functions
4 |
5 | ```{eval-rst}
6 | .. automodule:: jupyter_releaser.lib
7 | :members:
8 | ```
9 |
10 | ## Python Utility Functions
11 |
12 | ```{eval-rst}
13 | .. automodule:: jupyter_releaser.python
14 | :members:
15 |
16 | ```
17 |
18 | ## NPM Utility Functions
19 |
20 | ```{eval-rst}
21 | .. automodule:: jupyter_releaser.npm
22 | :members:
23 |
24 | ```
25 |
26 | ## Changelog Utility Functions
27 |
28 | ```{eval-rst}
29 | .. automodule:: jupyter_releaser.changelog
30 | :members:
31 |
32 | ```
33 |
34 | ## Global Utility Functions
35 |
36 | ```{eval-rst}
37 | .. automodule:: jupyter_releaser.util
38 | :members:
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/source/reference/changelog.md:
--------------------------------------------------------------------------------
1 | ```{include} ../../../CHANGELOG.md
2 | ```
3 |
--------------------------------------------------------------------------------
/docs/source/reference/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | Coming Soon!
4 |
--------------------------------------------------------------------------------
/docs/source/reference/index.md:
--------------------------------------------------------------------------------
1 | # Reference
2 |
3 | Technical reference. Covers tools, components, commands, and resources.
4 |
5 | ```{toctree}
6 | :maxdepth: 1
7 | :caption: Contents:
8 |
9 | releaser_cli
10 | api_docs
11 | configuration
12 | theory
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/source/reference/releaser_cli.md:
--------------------------------------------------------------------------------
1 | # Releaser CLI
2 |
3 | ```{click} jupyter_releaser.cli:main
4 | :prog: jupyter-releaser
5 | :nested: full
6 | ```
7 |
--------------------------------------------------------------------------------
/docs/source/reference/theory.md:
--------------------------------------------------------------------------------
1 | # Theory
2 |
3 | ## Motivation
4 |
5 | This project should help maintainers reduce toil and save time in the release process by enforcing best practices to:
6 |
7 | - Automate a changelog for every release
8 | - Verify the install and import of dist asset(s)
9 | - Commit a message with hashes of dist file(s)
10 | - Annotate the git tag in standard format
11 | - Create a GitHub release with changelog entry
12 | - Forward port changelog entries into default branch
13 | - Dry run publish on CI
14 | - Revert to Dev version after release (optional)
15 |
16 | ## Security
17 |
18 | We strive to use the most secure release practices possible, reflected in the `Checklist for Adoption`
19 | and the example workflows.
20 | This includes using PyPI Trusted Publishing, using GitHub Environments, encouraging the use of Rulesets and GitHub Apps with limited bypass capability, and provenance data for npm.
21 | In addition, there is an automatic check for whether the user who triggered the action is an admin.
22 |
23 | ## Action Details
24 |
25 | Detailed workflows are available to draft a changelog, draft a release, publish a release, and check a release.
26 |
27 | ### Prep Release Action
28 |
29 | - Inputs are the target repo, branch, and the version spec
30 | - Bumps the version
31 | - By default, uses [hatch](https://hatch.pypa.io/latest/), [tbump](https://github.com/tankerhq/tbump) or [bump2version](https://github.com/c4urself/bump2version) to bump the version based on presence of config files
32 | - We recommend `hatch` for most cases because it is very easy to set up.
33 | - Prepares the environment
34 | - Sets up git config and branch
35 | - Generates a changelog (using [github-activity](https://github.com/executablebooks/github-activity)) using the PRs since the last tag on this branch.
36 | - Gets the current version and then does a git checkout to clear state
37 | - Adds a new version entry using a HTML comment markers in the changelog file
38 | - Optionally resolves [meeseeks](https://github.com/MeeseeksBox/MeeseeksDev) backport PRs to their original PR
39 | - Creates a Draft GitHub release with the changelog changes and an attached
40 | metadata.json file capturing the inputs to the workflow.
41 |
42 | ### Populate Release Action
43 |
44 | - Input is typically the URL of the draft GitHub Release created in the Prep Release workflow, or no input to use the most recent draft release.
45 | - Fetches the `metadata.json` file and the changelog entry from the draft
46 | release.
47 | - Prepares the environment using the same method as the changelog action
48 | - Bumps the version
49 | - For Python packages:
50 | - Builds the wheel and source distributions if applicable
51 | - Makes sure Python dists can be installed and imported in a virtual environment
52 | - For npm package(s) (including workspace support):
53 | - Builds tarball(s) using `npm pack`
54 | - Make sure tarball(s) can be installed and imported in a new npm package
55 | - Adds a commit that includes the hashes of the dist files
56 | - Creates an annotated version tag in standard format
57 | - If given, bumps the version using the post version spec. he post version
58 | spec can also be given as a setting, [Write Releaser Config Guide](../how_to_guides/write_config.md).
59 | - Verifies that the SHA of the most recent commit has not changed on the target
60 | branch, preventing a mismatch of release commit.
61 | - Pushes the commits and tag to the target `branch`
62 | - Pusehes the created assets to the draft release, along with an `asset_shas.json` file capturing the checksums of the files.
63 |
64 | ### Finalize Release Action
65 |
66 | - Input is the url of the draft GitHub release from the Populate Release
67 | action.
68 | - Downloads the dist assets from the release
69 | - Verifies shas of release assets against the `asset_shas.json` file.
70 | - Publishes assets to appropriate registries.
71 | - Publishes the final GitHub release
72 | - If the tag is on a backport branch, makes a forwardport PR for the changelog entry
73 |
74 | Typically the Populate Release action and Finalize release action are
75 | run as part of the same workflow.
76 |
77 | ### Check Release Action
78 |
79 | - Runs on CI in the target repository to verify compatibility and release-ability.
80 | - Runs the `Draft Changelog` and `Draft Release` actions in dry run mode
81 | - Publishes to the local PyPI server and/or dry-run `npm publish`.
82 | - Does not make PRs or push git changes
83 |
--------------------------------------------------------------------------------
/example-workflows/full-release.yml:
--------------------------------------------------------------------------------
1 | name: "Steps 1 + 2: Full Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version_spec:
6 | description: "New Version Specifier"
7 | default: "next"
8 | required: false
9 | branch:
10 | description: "The branch to target"
11 | required: false
12 | post_version_spec:
13 | description: "Post Version Specifier"
14 | required: false
15 | # silent:
16 | # description: "Set a placeholder in the changelog and don't publish the release."
17 | # required: false
18 | # type: boolean
19 | since:
20 | description: "Use PRs with activity since this date or git reference"
21 | required: false
22 | since_last_stable:
23 | description: "Use PRs with activity since the last stable git tag"
24 | required: false
25 | type: boolean
26 | steps_to_skip:
27 | description: "Comma separated list of steps to skip during Populate Release"
28 | required: false
29 | jobs:
30 | prep_release:
31 | runs-on: ubuntu-latest
32 | permissions:
33 | contents: write
34 | steps:
35 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
36 |
37 | - name: Prep Release
38 | id: prep-release
39 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
40 | with:
41 | token: ${{ secrets.GITHUB_TOKEN }}
42 | version_spec: ${{ github.event.inputs.version_spec }}
43 | # silent: ${{ github.event.inputs.silent }}
44 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
45 | target: ${{ github.event.inputs.target }}
46 | branch: ${{ github.event.inputs.branch }}
47 | since: ${{ github.event.inputs.since }}
48 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
49 |
50 | publish_release:
51 | needs: [prep_release]
52 | runs-on: ubuntu-latest
53 | environment: release
54 | permissions:
55 | id-token: write
56 | steps:
57 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
58 |
59 | - uses: actions/create-github-app-token@v1
60 | id: app-token
61 | with:
62 | app-id: ${{ vars.APP_ID }}
63 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
64 |
65 | - name: Populate Release
66 | id: populate-release
67 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
68 | with:
69 | token: ${{ steps.app-token.outputs.token }}
70 | target: ${{ github.event.inputs.target }}
71 | branch: ${{ github.event.inputs.branch }}
72 | release_url: ${{ github.event.inputs.release_url }}
73 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
74 |
75 | - name: Finalize Release
76 | id: finalize-release
77 | env:
78 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
79 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
80 | with:
81 | token: ${{ steps.app-token.outputs.token }}
82 | target: ${{ github.event.inputs.target }}
83 | release_url: ${{ steps.populate-release.outputs.release_url }}
84 |
85 | - name: "** Next Step **"
86 | if: ${{ success() }}
87 | run: |
88 | echo "Verify the final release"
89 | echo ${{ steps.finalize-release.outputs.release_url }}
90 |
91 | - name: "** Failure Message **"
92 | if: ${{ failure() }}
93 | run: |
94 | echo "Failed to Publish the Draft Release Url:"
95 | echo ${{ steps.populate-release.outputs.release_url }}
96 |
--------------------------------------------------------------------------------
/example-workflows/prep-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 1: Prep Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | version_spec:
6 | description: "New Version Specifier"
7 | default: "next"
8 | required: false
9 | branch:
10 | description: "The branch to target"
11 | required: false
12 | post_version_spec:
13 | description: "Post Version Specifier"
14 | required: false
15 | # silent:
16 | # description: "Set a placeholder in the changelog and don't publish the release."
17 | # required: false
18 | # type: boolean
19 | since:
20 | description: "Use PRs with activity since this date or git reference"
21 | required: false
22 | since_last_stable:
23 | description: "Use PRs with activity since the last stable git tag"
24 | required: false
25 | type: boolean
26 | jobs:
27 | prep_release:
28 | runs-on: ubuntu-latest
29 | permissions:
30 | contents: write
31 | steps:
32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
33 |
34 | - name: Prep Release
35 | id: prep-release
36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2
37 | with:
38 | token: ${{ secrets.GITHUB_TOKEN }}
39 | version_spec: ${{ github.event.inputs.version_spec }}
40 | # silent: ${{ github.event.inputs.silent }}
41 | post_version_spec: ${{ github.event.inputs.post_version_spec }}
42 | branch: ${{ github.event.inputs.branch }}
43 | since: ${{ github.event.inputs.since }}
44 | since_last_stable: ${{ github.event.inputs.since_last_stable }}
45 |
46 | - name: "** Next Step **"
47 | run: |
48 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}"
49 |
--------------------------------------------------------------------------------
/example-workflows/publish-changelog.yml:
--------------------------------------------------------------------------------
1 | name: "Publish Changelog"
2 | on:
3 | release:
4 | types: [published]
5 |
6 | workflow_dispatch:
7 | inputs:
8 | branch:
9 | description: "The branch to target"
10 | required: false
11 |
12 | jobs:
13 | publish_changelog:
14 | runs-on: ubuntu-latest
15 | environment: release
16 | steps:
17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
18 |
19 | - uses: actions/create-github-app-token@v1
20 | id: app-token
21 | with:
22 | app-id: ${{ vars.APP_ID }}
23 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
24 |
25 | - name: Publish changelog
26 | id: publish-changelog
27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2
28 | with:
29 | token: ${{ steps.app-token.outputs.token }}
30 | branch: ${{ github.event.inputs.branch }}
31 |
32 | - name: "** Next Step **"
33 | run: |
34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}"
35 |
--------------------------------------------------------------------------------
/example-workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: "Step 2: Publish Release"
2 | on:
3 | workflow_dispatch:
4 | inputs:
5 | branch:
6 | description: "The target branch"
7 | required: false
8 | release_url:
9 | description: "The URL of the draft GitHub release"
10 | required: false
11 | steps_to_skip:
12 | description: "Comma separated list of steps to skip"
13 | required: false
14 |
15 | jobs:
16 | publish_release:
17 | runs-on: ubuntu-latest
18 | environment: release
19 | permissions:
20 | id-token: write
21 | steps:
22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
23 |
24 | - uses: actions/create-github-app-token@v1
25 | id: app-token
26 | with:
27 | app-id: ${{ vars.APP_ID }}
28 | private-key: ${{ secrets.APP_PRIVATE_KEY }}
29 |
30 | - name: Populate Release
31 | id: populate-release
32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2
33 | with:
34 | token: ${{ steps.app-token.outputs.token }}
35 | branch: ${{ github.event.inputs.branch }}
36 | release_url: ${{ github.event.inputs.release_url }}
37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }}
38 |
39 | - name: Finalize Release
40 | id: finalize-release
41 | env:
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
44 | with:
45 | token: ${{ steps.app-token.outputs.token }}
46 | release_url: ${{ steps.populate-release.outputs.release_url }}
47 |
48 | - name: "** Next Step **"
49 | if: ${{ success() }}
50 | run: |
51 | echo "Verify the final release"
52 | echo ${{ steps.finalize-release.outputs.release_url }}
53 |
54 | - name: "** Failure Message **"
55 | if: ${{ failure() }}
56 | run: |
57 | echo "Failed to Publish the Draft Release Url:"
58 | echo ${{ steps.populate-release.outputs.release_url }}
59 |
--------------------------------------------------------------------------------
/jupyter_releaser/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Jupyter Development Team.
2 | # Distributed under the terms of the Modified BSD License.
3 | __version__ = "1.9.0.dev0"
4 |
--------------------------------------------------------------------------------
/jupyter_releaser/__main__.py:
--------------------------------------------------------------------------------
1 | """The main entry point for Jupyter Releaser."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | from jupyter_releaser.cli import main
5 |
6 | if __name__ == "__main__":
7 | main()
8 |
--------------------------------------------------------------------------------
/jupyter_releaser/actions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/jupyter_releaser/actions/__init__.py
--------------------------------------------------------------------------------
/jupyter_releaser/actions/common.py:
--------------------------------------------------------------------------------
1 | """Common functions for actions."""
2 | from contextlib import contextmanager
3 |
4 | from jupyter_releaser.util import prepare_environment
5 | from jupyter_releaser.util import run as _run
6 |
7 |
8 | @contextmanager
9 | def make_group(name):
10 | """Make a collapsed group in the GitHub Actions log."""
11 | print(f"::group::{name}")
12 | yield
13 | print("::endgroup::")
14 |
15 |
16 | def setup(fetch_draft_release=True):
17 | """Common setup tasks for actions."""
18 | with make_group("Prepare Environment"):
19 | return prepare_environment(fetch_draft_release=fetch_draft_release)
20 |
21 |
22 | def run_action(target, *args, **kwargs):
23 | """Run an action."""
24 | with make_group(target):
25 | _run(target, *args, **kwargs)
26 |
--------------------------------------------------------------------------------
/jupyter_releaser/actions/finalize_release.py:
--------------------------------------------------------------------------------
1 | """Finalize a release."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | import os
5 |
6 | from jupyter_releaser.actions.common import run_action, setup
7 |
8 | setup()
9 |
10 | release_url = os.environ["RH_RELEASE_URL"]
11 |
12 | if release_url:
13 | run_action("jupyter-releaser extract-release")
14 |
15 | run_action("jupyter-releaser publish-assets")
16 |
17 | if release_url:
18 | run_action("jupyter-releaser forwardport-changelog")
19 | run_action("jupyter-releaser publish-release")
20 |
--------------------------------------------------------------------------------
/jupyter_releaser/actions/generate_changelog.py:
--------------------------------------------------------------------------------
1 | """Generate a changelog."""
2 | import os
3 | from pathlib import Path
4 |
5 | from jupyter_releaser.actions.common import run_action
6 | from jupyter_releaser.changelog import get_version_entry
7 | from jupyter_releaser.util import CHECKOUT_NAME, get_branch, handle_since, log
8 |
9 | target = os.environ.get("RH_REPOSITORY")
10 | branch = os.environ.get("RH_BRANCH", "")
11 | ref = os.environ.get("RH_REF")
12 | since = handle_since()
13 | until = os.environ.get("RH_UNTIL")
14 | convert_to_rst = os.environ.get("RH_CONVERT_TO_RST", "")
15 |
16 | log("Generating changelog")
17 | log("target:", target)
18 | log("branch:", branch)
19 | log("ref:", ref)
20 | log("since:", since)
21 | log("until:", until)
22 | log("convert to rst:", convert_to_rst.lower() == "true")
23 |
24 | run_action("jupyter-releaser prep-git")
25 | branch = get_branch()
26 | orig_dir = os.getcwd()
27 | os.chdir(CHECKOUT_NAME)
28 | output = get_version_entry(ref, branch, target, "current", since=since, until=until)
29 |
30 | if convert_to_rst.lower() == "true":
31 | from pypandoc import convert_text # type:ignore[import-not-found]
32 |
33 | output = convert_text(output, "rst", "markdown")
34 | log("\n\n------------------------------")
35 | log(output, "------------------------------\n\n")
36 | os.chdir(orig_dir)
37 | Path("CHANGELOG_ENTRY.md").write_text(output, encoding="utf-8")
38 |
--------------------------------------------------------------------------------
/jupyter_releaser/actions/populate_release.py:
--------------------------------------------------------------------------------
1 | """Populate a release."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 |
5 | import os
6 | import sys
7 |
8 | from jupyter_releaser.actions.common import run_action, setup
9 | from jupyter_releaser.util import actions_output, get_gh_object, log, release_for_url
10 |
11 | setup()
12 |
13 | # Skip if we already have asset shas.
14 | release_url = os.environ["RH_RELEASE_URL"]
15 | owner, repo = os.environ["RH_REPOSITORY"].split("/")
16 | auth = os.environ["GITHUB_ACCESS_TOKEN"]
17 | gh = get_gh_object(False, owner=owner, repo=repo, token=auth)
18 | release = release_for_url(gh, release_url)
19 | for asset in release.assets:
20 | if asset.name == "asset_shas.json":
21 | log("Skipping populate assets")
22 | actions_output("release_url", release_url)
23 | sys.exit(0)
24 |
25 | dry_run = os.environ.get("RH_DRY_RUN", "").lower() == "true"
26 |
27 | if not os.environ.get("RH_RELEASE_URL"):
28 | msg = "Cannot complete Draft Release, no draft GitHub release url found!"
29 | raise RuntimeError(msg)
30 |
31 | run_action("jupyter-releaser prep-git")
32 | run_action("jupyter-releaser ensure-sha")
33 | run_action("jupyter-releaser bump-version")
34 | run_action("jupyter-releaser extract-changelog")
35 |
36 | # Make sure npm comes before python in case it produces
37 | # files for the python package
38 | run_action("jupyter-releaser build-npm")
39 | run_action("jupyter-releaser check-npm")
40 | run_action("jupyter-releaser build-python")
41 | run_action("jupyter-releaser check-python")
42 | run_action("jupyter-releaser tag-release")
43 | run_action("jupyter-releaser ensure-sha")
44 | run_action("jupyter-releaser populate-release")
45 |
--------------------------------------------------------------------------------
/jupyter_releaser/actions/prep_release.py:
--------------------------------------------------------------------------------
1 | """Prepare a release."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | import os
5 |
6 | from jupyter_releaser.actions.common import make_group, run_action, setup
7 | from jupyter_releaser.util import CHECKOUT_NAME, get_default_branch, handle_since
8 |
9 | setup(False)
10 |
11 | run_action("jupyter-releaser prep-git")
12 |
13 | # Handle the branch.
14 | if not os.environ.get("RH_BRANCH"):
15 | cur_dir = os.getcwd()
16 | os.chdir(CHECKOUT_NAME)
17 | os.environ["RH_BRANCH"] = get_default_branch() or ""
18 | os.chdir(cur_dir)
19 |
20 | # Capture the "since" variable in case we add tags before checking changelog
21 | # Do this before bumping the version.
22 | with make_group("Handle RH_SINCE"):
23 | handle_since()
24 |
25 | run_action("jupyter-releaser bump-version")
26 | run_action("jupyter-releaser build-changelog")
27 | run_action("jupyter-releaser draft-changelog")
28 |
--------------------------------------------------------------------------------
/jupyter_releaser/actions/publish_changelog.py:
--------------------------------------------------------------------------------
1 | """Remove silent placeholder entries in the changelog."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | import os
5 |
6 | from jupyter_releaser.actions.common import run_action, setup
7 | from jupyter_releaser.util import CHECKOUT_NAME, get_default_branch
8 |
9 | setup(False)
10 |
11 | run_action("jupyter-releaser prep-git")
12 |
13 | # Handle the branch.
14 | if not os.environ.get("RH_BRANCH"):
15 | cur_dir = os.getcwd()
16 | os.chdir(CHECKOUT_NAME)
17 | os.environ["RH_BRANCH"] = get_default_branch() or ""
18 | os.chdir(cur_dir)
19 |
20 | run_action("jupyter-releaser publish-changelog")
21 |
--------------------------------------------------------------------------------
/jupyter_releaser/changelog.py:
--------------------------------------------------------------------------------
1 | """Changelog utilities for Jupyter Releaser."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | import re
5 | from pathlib import Path
6 | from typing import Optional
7 |
8 | import mdformat
9 | from fastcore.net import HTTP404NotFoundError # type:ignore[import-untyped]
10 | from github_activity import generate_activity_md # type:ignore[import-untyped]
11 |
12 | from jupyter_releaser import util
13 |
14 | START_MARKER = ""
15 | END_MARKER = ""
16 | START_SILENT_MARKER = ""
17 | END_SILENT_MARKER = ""
18 | PR_PREFIX = "Automated Changelog Entry"
19 | PRECOMMIT_PREFIX = "[pre-commit.ci] pre-commit autoupdate"
20 |
21 |
22 | def format_pr_entry(target, number, auth=None, dry_run=False):
23 | """Format a PR entry in the style used by our changelogs.
24 |
25 | Parameters
26 | ----------
27 | target : str
28 | The GitHub owner/repo
29 | number : int
30 | The PR number to resolve
31 | auth : str, optional
32 | The GitHub authorization token
33 | dry_run: bool, optional
34 | Whether this is a dry run.
35 |
36 | Returns
37 | -------
38 | str
39 | A formatted PR entry
40 | """
41 | owner, repo = target.split("/")
42 | gh = util.get_gh_object(dry_run=dry_run, owner=owner, repo=repo, token=auth)
43 | pull = gh.pulls.get(number)
44 | title = pull.title
45 | url = pull.html_url
46 | user_name = pull.user.login
47 | user_url = pull.user.html_url
48 | return f"- {title} [#{number}]({url}) ([@{user_name}]({user_url}))"
49 |
50 |
51 | def get_version_entry(
52 | ref,
53 | branch,
54 | repo,
55 | version,
56 | *,
57 | since=None,
58 | since_last_stable=None,
59 | until=None,
60 | auth=None,
61 | resolve_backports=False, # noqa: ARG001
62 | dry_run=False,
63 | ):
64 | """Get a changelog for the changes since the last tag on the given branch.
65 |
66 | Parameters
67 | ----------
68 | branch : str
69 | The target branch
70 | ref: str
71 | The source reference
72 | repo : str
73 | The GitHub owner/repo
74 | version : str
75 | The new version
76 | since: str
77 | Use PRs with activity since this date or git reference
78 | since_last_stable:
79 | Use PRs with activity since the last stable git tag
80 | until: str, optional
81 | Use PRs until this date or git reference
82 | auth : str, optional
83 | The GitHub authorization token
84 | resolve_backports: bool, optional
85 | Whether to resolve backports to the original PR
86 | dry_run: bool, optional
87 | Whether this is a dry run.
88 |
89 | Returns
90 | -------
91 | str
92 | A formatted changelog entry with markers
93 | """
94 | branch = branch or util.get_branch()
95 | since = since or util.get_latest_tag(ref or branch, since_last_stable)
96 |
97 | if since == "":
98 | since = util.get_first_commit(ref or branch)
99 |
100 | util.log(f"Getting changes to {repo} since {since} on branch {branch}...")
101 |
102 | until = until.replace("%", "") if until else None
103 |
104 | md = generate_activity_md(
105 | repo,
106 | since=since,
107 | until=until,
108 | kind="pr",
109 | heading_level=2,
110 | auth=auth,
111 | branch=branch,
112 | )
113 |
114 | if not md:
115 | util.log("No PRs found")
116 | return f"## {version}\n\nNo merged PRs"
117 |
118 | entry = md.replace("[full changelog]", "[Full Changelog]")
119 |
120 | entry = entry.replace("...None", f"...{until}") if until else entry.replace("...None", "")
121 |
122 | entry = entry.splitlines()[2:]
123 |
124 | for ind, line in enumerate(entry):
125 | # Look for a backport, either manual or automatic.
126 | match = re.search(r"Backport PR #(\d+) on branch", line)
127 | if match:
128 | entry[ind] = format_pr_entry(repo, match.groups()[0], auth=auth, dry_run=dry_run)
129 |
130 | # Remove github actions PRs
131 | gh_actions = "[@github-actions](https://github.com/github-actions)"
132 | entry = [e for e in entry if gh_actions not in e]
133 |
134 | # Remove automated changelog PRs
135 | entry = [e for e in entry if PR_PREFIX not in e]
136 |
137 | # Remove Pre-Commit PRs
138 | entry = [e for e in entry if PRECOMMIT_PREFIX not in e]
139 |
140 | entry = "\n".join(entry).strip()
141 |
142 | # Remove empty documentation entry if only automated changelogs were there
143 | if "# Documentation improvements" in entry and "# Documentation improvements\n\n-" not in entry:
144 | entry = re.sub(r"#+ Documentation improvements\n\n", "", entry)
145 |
146 | return f"""
147 | ## {version}
148 |
149 | {entry}
150 | """.strip()
151 |
152 |
153 | def build_entry(
154 | ref, branch, repo, auth, changelog_path, since, since_last_stable, resolve_backports
155 | ):
156 | """Build a python version entry"""
157 | branch = branch or util.get_branch()
158 | repo = repo or util.get_repo()
159 |
160 | # Get the new version
161 | version = util.get_version()
162 |
163 | # Get changelog entry
164 | entry = get_version_entry(
165 | ref,
166 | branch,
167 | repo,
168 | version,
169 | since=since,
170 | since_last_stable=since_last_stable,
171 | auth=auth,
172 | resolve_backports=resolve_backports,
173 | )
174 | update_changelog(changelog_path, entry)
175 |
176 |
177 | def update_changelog(changelog_path, entry, silent=False):
178 | """Update a changelog with a new entry."""
179 | # Get the new version
180 | version = util.get_version()
181 |
182 | # Get the existing changelog and run some validation
183 | changelog = Path(changelog_path).read_text(encoding="utf-8")
184 |
185 | if START_MARKER not in changelog or END_MARKER not in changelog:
186 | msg = "Missing insert marker for changelog"
187 | raise ValueError(msg)
188 |
189 | if changelog.find(START_MARKER) != changelog.rfind(START_MARKER):
190 | msg = "Insert marker appears more than once in changelog"
191 | raise ValueError(msg)
192 |
193 | changelog = insert_entry(changelog, entry, version=version, silent=silent)
194 | Path(changelog_path).write_text(changelog, encoding="utf-8")
195 |
196 |
197 | def remove_placeholder_entries(
198 | repo: str,
199 | auth: Optional[str],
200 | changelog_path: str,
201 | dry_run: bool,
202 | ) -> int:
203 | """Replace any silent marker with the GitHub release body
204 | if the release has been published.
205 |
206 | Parameters
207 | ----------
208 | repo : str
209 | The GitHub owner/repo
210 | auth : str
211 | The GitHub authorization token
212 | changelog_path : str
213 | The changelog file path
214 | dry_run: bool
215 |
216 | Returns
217 | -------
218 | int
219 | Number of placeholders removed
220 | """
221 |
222 | changelog = Path(changelog_path).read_text(encoding="utf-8")
223 | start_count = changelog.count(START_SILENT_MARKER)
224 | end_count = changelog.count(END_SILENT_MARKER)
225 | if start_count != end_count:
226 | msg = ""
227 | raise ValueError(msg)
228 |
229 | repo = repo or util.get_repo()
230 | owner, repo_name = repo.split("/")
231 | gh = util.get_gh_object(dry_run=dry_run, owner=owner, repo=repo_name, token=auth)
232 |
233 | # Replace silent placeholder by release body if it has been published
234 | previous_index = None
235 | changes_count = 0
236 | for _ in range(start_count):
237 | start = changelog.index(START_SILENT_MARKER, previous_index)
238 | end = changelog.index(END_SILENT_MARKER, start)
239 |
240 | version = _extract_version(changelog[start + len(START_SILENT_MARKER) : end])
241 | try:
242 | util.log(f"Getting release for tag '{version}'...")
243 | release = gh.repos.get_release_by_tag(owner=owner, repo=repo_name, tag=f"v{version}")
244 | except HTTP404NotFoundError:
245 | # Skip this version
246 | pass
247 | else:
248 | if not release.draft:
249 | changelog_text = mdformat.text(release.body)
250 | changelog = (
251 | changelog[:start]
252 | + f"\n\n{changelog_text}\n\n"
253 | + changelog[end + len(END_SILENT_MARKER) :]
254 | )
255 | changes_count += 1
256 |
257 | previous_index = end
258 |
259 | # Write back the new changelog
260 | Path(changelog_path).write_text(format(changelog), encoding="utf-8")
261 | return changes_count
262 |
263 |
264 | def insert_entry(
265 | changelog: str, entry: str, version: Optional[str] = None, silent: bool = False
266 | ) -> str:
267 | """Insert the entry into the existing changelog."""
268 | # Test if we are augmenting an existing changelog entry (for new PRs)
269 | # Preserve existing PR entries since we may have formatted them
270 | if silent:
271 | entry = f"{START_SILENT_MARKER}\n\n## {version}\n\n{END_SILENT_MARKER}"
272 |
273 | new_entry = f"{START_MARKER}\n\n{entry}\n\n{END_MARKER}"
274 | prev_entry = changelog[
275 | changelog.index(START_MARKER) : changelog.index(END_MARKER) + len(END_MARKER)
276 | ]
277 |
278 | if f"# {version}\n" in prev_entry:
279 | lines = new_entry.splitlines()
280 | old_lines = prev_entry.splitlines()
281 | for ind, line in enumerate(lines):
282 | pr = re.search(r"\[#\d+\]", line)
283 | if not pr:
284 | continue
285 | for old_line in old_lines:
286 | if pr.group() in old_line:
287 | lines[ind] = old_line
288 | changelog = changelog.replace(prev_entry, "\n".join(lines))
289 | else:
290 | changelog = changelog.replace(END_MARKER, "")
291 | changelog = changelog.replace(START_MARKER, new_entry)
292 |
293 | return format(changelog)
294 |
295 |
296 | def format(changelog: str) -> str:
297 | """Clean up changelog formatting"""
298 | changelog = re.sub(r"\n\n+", r"\n\n", changelog)
299 | return re.sub(r"\n\n+$", r"\n", changelog)
300 |
301 |
302 | def check_entry(
303 | ref,
304 | branch,
305 | repo,
306 | auth,
307 | changelog_path,
308 | since,
309 | since_last_stable,
310 | resolve_backports,
311 | output,
312 | ):
313 | """Check changelog entry"""
314 | branch = branch or util.get_branch()
315 |
316 | # Get the new version
317 | version = util.get_version()
318 |
319 | # Finalize changelog
320 | changelog = Path(changelog_path).read_text(encoding="utf-8")
321 |
322 | start = changelog.find(START_MARKER)
323 | end = changelog.find(END_MARKER)
324 |
325 | if start == -1 or end == -1: # pragma: no cover
326 | msg = "Missing new changelog entry delimiter(s)"
327 | raise ValueError(msg)
328 |
329 | if start != changelog.rfind(START_MARKER): # pragma: no cover
330 | msg = "Insert marker appears more than once in changelog"
331 | raise ValueError(msg)
332 |
333 | final_entry = changelog[start + len(START_MARKER) : end]
334 |
335 | repo = repo or util.get_repo()
336 |
337 | raw_entry = get_version_entry(
338 | ref,
339 | branch,
340 | repo,
341 | version,
342 | since=since,
343 | since_last_stable=since_last_stable,
344 | auth=auth,
345 | resolve_backports=resolve_backports,
346 | )
347 |
348 | if f"# {version}" not in final_entry: # pragma: no cover
349 | util.log(final_entry)
350 | msg = f"Did not find entry for {version}"
351 | raise ValueError(msg)
352 |
353 | final_prs = re.findall(r"\[#(\d+)\]", final_entry)
354 | raw_prs = re.findall(r"\[#(\d+)\]", raw_entry)
355 |
356 | for pr in raw_prs:
357 | # Allow for changelog PR to not be in changelog itself
358 | skip = False
359 | for line in raw_entry.splitlines():
360 | if f"[#{pr}]" in line and "changelog" in line.lower():
361 | skip = True
362 | break
363 | if skip:
364 | continue
365 | if f"[#{pr}]" not in final_entry: # pragma: no cover
366 | msg = f"Missing PR #{pr} in changelog"
367 | raise ValueError(msg)
368 | for pr in final_prs:
369 | if f"[#{pr}]" not in raw_entry: # pragma: no cover
370 | msg = f"PR #{pr} does not belong in changelog for {version}"
371 | raise ValueError(msg)
372 |
373 | if output:
374 | Path(output).write_text(final_entry, encoding="utf-8")
375 |
376 |
377 | def splice_github_entry(orig_entry, github_entry):
378 | """Splice an entry created on GitHub into one created by build_entry"""
379 |
380 | # Override PR titles
381 | gh_regex = re.compile(r"^\* (.*?) by @.*?/pull/(\d+)$", flags=re.MULTILINE)
382 | cl_regex = re.compile(r"^- (.*?) \[#(\d+)\]")
383 |
384 | lut = {}
385 | for title, pr in re.findall(gh_regex, github_entry):
386 | lut[pr] = title
387 |
388 | lines = orig_entry.splitlines()
389 | for ind, line in enumerate(lines):
390 | match = re.match(cl_regex, line)
391 | if not match:
392 | continue
393 | title, pr = re.findall(cl_regex, line)[0]
394 | if pr in lut:
395 | lines[ind] = line.replace(title, lut[pr])
396 |
397 | # Handle preamble
398 | preamble_index = github_entry.index("## What's Changed")
399 | if preamble_index > 0:
400 | preamble = github_entry[:preamble_index]
401 | if preamble.startswith("# "):
402 | preamble = preamble.replace("# ", "## ")
403 | if preamble.startswith("## "):
404 | preamble = preamble.replace("## ", "### ")
405 |
406 | lines = [*preamble.splitlines(), "", *lines]
407 |
408 | return "\n".join(lines)
409 |
410 |
411 | def extract_current(changelog_path):
412 | """Extract the current changelog entry"""
413 | body = ""
414 | if changelog_path and Path(changelog_path).exists():
415 | changelog = Path(changelog_path).read_text(encoding="utf-8")
416 |
417 | start = changelog.find(START_MARKER)
418 | end = changelog.find(END_MARKER)
419 | if start != -1 and end != -1:
420 | body = changelog[start + len(START_MARKER) : end]
421 | return body
422 |
423 |
424 | def extract_current_version(changelog_path):
425 | """Extract the current released version from the changelog"""
426 | body = extract_current(changelog_path)
427 | return _extract_version(body)
428 |
429 |
430 | def _extract_version(entry: str) -> str:
431 | """Extract version from entry"""
432 | match = re.match(r"#+ (\d\S+)", entry.strip())
433 | if not match:
434 | msg = "Could not find previous version"
435 | raise ValueError(msg)
436 | return match.groups()[0]
437 |
--------------------------------------------------------------------------------
/jupyter_releaser/mock_github.py:
--------------------------------------------------------------------------------
1 | """A mock GitHub API implementation."""
2 | import atexit
3 | import json
4 | import os
5 | import tempfile
6 | import uuid
7 | from datetime import datetime, timezone
8 | from typing import Dict, List
9 |
10 | from fastapi import FastAPI, Request
11 | from fastapi.staticfiles import StaticFiles
12 | from pydantic import BaseModel
13 |
14 | from jupyter_releaser.util import get_mock_github_url
15 |
16 | app = FastAPI()
17 |
18 | if "RH_GITHUB_STATIC_DIR" in os.environ:
19 | static_dir = os.environ["RH_GITHUB_STATIC_DIR"]
20 | else:
21 | static_dir_obj = tempfile.TemporaryDirectory()
22 | atexit.register(static_dir_obj.cleanup)
23 | static_dir = static_dir_obj.name
24 |
25 | app.mount("/static", StaticFiles(directory=static_dir), name="static")
26 |
27 |
28 | def load_from_file(name, klass):
29 | """Load data from a file to a model."""
30 | source_file = os.path.join(static_dir, name + ".json")
31 | if not os.path.exists(source_file):
32 | return {}
33 | with open(source_file) as fid:
34 | data = json.load(fid)
35 | results = {}
36 | for key in data:
37 | if issubclass(klass, BaseModel):
38 | results[key] = klass(**data[key])
39 | else:
40 | results[key] = data[key]
41 | return results
42 |
43 |
44 | def write_to_file(name, data):
45 | """Write model data to a file."""
46 | source_file = os.path.join(static_dir, name + ".json")
47 | result = {}
48 | for key in data:
49 | value = data[key]
50 | if isinstance(value, BaseModel):
51 | if hasattr(value, "model_dump_json"):
52 | value = json.loads(value.model_dump_json())
53 | else:
54 | value = json.loads(value.json())
55 | result[key] = value
56 | with open(source_file, "w") as fid:
57 | json.dump(result, fid)
58 |
59 |
60 | class Asset(BaseModel):
61 | """An asset model."""
62 |
63 | id: int
64 | name: str
65 | content_type: str
66 | size: int
67 | state: str = "uploaded"
68 | url: str
69 | node_id: str = ""
70 | download_count: int = 0
71 | label: str = ""
72 | uploader: None = None
73 | browser_download_url: str = ""
74 | created_at: str = ""
75 | updated_at: str = ""
76 |
77 |
78 | class Release(BaseModel):
79 | """A release model."""
80 |
81 | assets_url: str = ""
82 | upload_url: str
83 | tarball_url: str = ""
84 | zipball_url: str = ""
85 | created_at: str
86 | published_at: str = ""
87 | draft: bool
88 | body: str = ""
89 | id: int
90 | node_id: str = ""
91 | author: str = ""
92 | html_url: str
93 | name: str = ""
94 | prerelease: bool
95 | tag_name: str
96 | target_commitish: str
97 | assets: List[Asset]
98 | url: str
99 |
100 |
101 | class User(BaseModel):
102 | """A user model."""
103 |
104 | login: str = "bar"
105 | html_url: str = "http://bar.com"
106 |
107 |
108 | class PullRequest(BaseModel):
109 | """A pull request model."""
110 |
111 | number: int = 0
112 | html_url: str = "http://foo.com"
113 | title: str = "foo"
114 | user: User = User()
115 |
116 |
117 | class TagObject(BaseModel):
118 | """A tab object model."""
119 |
120 | sha: str
121 |
122 |
123 | class Tag(BaseModel):
124 | """A tag model."""
125 |
126 | ref: str
127 | object: TagObject
128 |
129 |
130 | releases: Dict[str, "Release"] = load_from_file("releases", Release)
131 | pulls: Dict[str, "PullRequest"] = load_from_file("pulls", PullRequest)
132 | release_ids_for_asset: Dict[str, str] = load_from_file("release_ids_for_asset", int)
133 | tag_refs: Dict[str, "Tag"] = load_from_file("tag_refs", Tag)
134 |
135 |
136 | @app.get("/")
137 | def read_root():
138 | """Get the root handler."""
139 | return {"Hello": "World"}
140 |
141 |
142 | @app.get("/repos/{owner}/{repo}/releases")
143 | def list_releases(owner: str, repo: str) -> List[Release]:
144 | """https://docs.github.com/en/rest/releases/releases#list-releases"""
145 | return list(releases.values())
146 |
147 |
148 | @app.get("/repos/{owner}/{repo}/releases/tags/{tag}")
149 | def get_release_by_tag(owner: str, repo: str, tag: str) -> Release:
150 | """https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name"""
151 | return next(filter(lambda r: r.tag_name == tag, releases.values()))
152 |
153 |
154 | @app.post("/repos/{owner}/{repo}/releases")
155 | async def create_a_release(owner: str, repo: str, request: Request) -> Release:
156 | """https://docs.github.com/en/rest/releases/releases#create-a-release"""
157 | release_id = uuid.uuid4().int % (2**32 - 1)
158 | data = await request.json()
159 | base_url = get_mock_github_url()
160 | url = f"{base_url}/repos/{owner}/{repo}/releases/{release_id}"
161 | html_url = f"{base_url}/{owner}/{repo}/releases/tag/{data['tag_name']}"
162 | upload_url = f"{base_url}/repos/{owner}/{repo}/releases/{release_id}/assets"
163 | fmt_str = r"%Y-%m-%dT%H:%M:%SZ"
164 | created_at = datetime.now(tz=timezone.utc).strftime(fmt_str)
165 | model = Release(
166 | id=release_id,
167 | url=url,
168 | html_url=html_url,
169 | assets=[],
170 | upload_url=upload_url,
171 | created_at=created_at,
172 | **data,
173 | )
174 | releases[str(model.id)] = model
175 | write_to_file("releases", releases)
176 | return model
177 |
178 |
179 | @app.patch("/repos/{owner}/{repo}/releases/{release_id}")
180 | async def update_a_release(owner: str, repo: str, release_id: int, request: Request) -> Release:
181 | """https://docs.github.com/en/rest/releases/releases#update-a-release"""
182 | data = await request.json()
183 | model = releases[str(release_id)]
184 | for name, value in data.items():
185 | setattr(model, name, value)
186 | write_to_file("releases", releases)
187 | return model
188 |
189 |
190 | @app.post("/repos/{owner}/{repo}/releases/{release_id}/assets")
191 | async def upload_a_release_asset(owner: str, repo: str, release_id: int, request: Request) -> None:
192 | """https://docs.github.com/en/rest/releases/assets#upload-a-release-asset"""
193 | base_url = get_mock_github_url()
194 | model = releases[str(release_id)]
195 | asset_id = uuid.uuid4().int % (2**32 - 1)
196 | name = request.query_params["name"]
197 | with open(f"{static_dir}/{asset_id}", "wb") as fid:
198 | async for chunk in request.stream():
199 | fid.write(chunk)
200 | headers = request.headers
201 | url = f"{base_url}/static/{asset_id}"
202 | asset = Asset(
203 | id=asset_id,
204 | name=name,
205 | size=int(headers["content-length"]),
206 | url=url,
207 | content_type=headers["content-type"],
208 | )
209 | release_ids_for_asset[str(asset_id)] = str(release_id)
210 | model.assets.append(asset)
211 | write_to_file("releases", releases)
212 | write_to_file("release_ids_for_asset", release_ids_for_asset)
213 |
214 |
215 | @app.delete("/repos/{owner}/{repo}/releases/assets/{asset_id}")
216 | async def delete_a_release_asset(owner: str, repo: str, asset_id: int) -> None:
217 | """https://docs.github.com/en/rest/releases/assets#delete-a-release-asset"""
218 | release = releases[release_ids_for_asset[str(asset_id)]]
219 | os.remove(f"{static_dir}/{asset_id}")
220 | release.assets = [a for a in release.assets if a.id != asset_id]
221 | del release_ids_for_asset[str(asset_id)]
222 | write_to_file("releases", releases)
223 | write_to_file("release_ids_for_asset", release_ids_for_asset)
224 |
225 |
226 | @app.delete("/repos/{owner}/{repo}/releases/{release_id}")
227 | def delete_a_release(owner: str, repo: str, release_id: int) -> None:
228 | """https://docs.github.com/en/rest/releases/releases#delete-a-release"""
229 | del releases[str(release_id)]
230 | write_to_file("releases", releases)
231 |
232 |
233 | @app.get("/repos/{owner}/{repo}/pulls/{pull_number}")
234 | def get_a_pull_request(owner: str, repo: str, pull_number: int) -> PullRequest:
235 | """https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request"""
236 | if str(pull_number) not in pulls:
237 | pulls[str(pull_number)] = PullRequest()
238 | write_to_file("pulls", pulls)
239 | return pulls[str(pull_number)]
240 |
241 |
242 | @app.post("/repos/{owner}/{repo}/pulls")
243 | def create_a_pull_request(owner: str, repo: str) -> PullRequest:
244 | """https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request"""
245 | pull = PullRequest()
246 | pulls[str(pull.number)] = pull
247 | write_to_file("pulls", pulls)
248 | return pull
249 |
250 |
251 | @app.post("/repos/{owner}/{repo}/issues/{issue_number}/labels")
252 | def add_labels_to_an_issue(owner: str, repo: str, issue_number: int) -> BaseModel:
253 | """https://docs.github.com/en/rest/issues/labels#add-labels-to-an-issue"""
254 |
255 | class _Inner(BaseModel):
256 | pass
257 |
258 | return _Inner()
259 |
260 |
261 | @app.post("/repos/{owner}/{repo}/git/refs")
262 | async def create_tag_ref(owner: str, repo: str, request: Request) -> None:
263 | """https://docs.github.com/en/rest/git/refs#create-a-reference"""
264 | data = await request.json()
265 | tag_ref = data["ref"]
266 | sha = data["sha"]
267 | tag = Tag(ref=f"refs/tags/{tag_ref}", object=TagObject(sha=sha))
268 | tag_refs[tag_ref] = tag
269 | write_to_file("tag_refs", tag_refs)
270 |
271 |
272 | @app.get("/repos/{owner}/{repo}/git/matching-refs/tags/{tag_ref}")
273 | def list_matching_references(owner: str, repo: str, tag_ref: str) -> List[Tag]:
274 | """https://docs.github.com/en/rest/git/refs#list-matching-references"""
275 | # raise ValueError("we should have an api to set a sha for a tag ref for tests")
276 | return [tag_refs[tag_ref]]
277 |
--------------------------------------------------------------------------------
/jupyter_releaser/npm.py:
--------------------------------------------------------------------------------
1 | """npm-related utilities."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | import json
5 | import os
6 | import os.path as osp
7 | import shutil
8 | import tarfile
9 | from glob import glob
10 | from pathlib import Path
11 | from tempfile import TemporaryDirectory
12 |
13 | from jupyter_releaser import util
14 |
15 | PACKAGE_JSON = util.PACKAGE_JSON
16 |
17 |
18 | # Python 3.12+ gives a deprecation warning if TarFile.extraction_filter is None.
19 | # https://docs.python.org/3.12/library/tarfile.html#tarfile-extraction-filter
20 | if hasattr(tarfile, "fully_trusted_filter"):
21 | tarfile.TarFile.extraction_filter = staticmethod(tarfile.fully_trusted_filter) # type:ignore[assignment]
22 |
23 |
24 | def build_dist(package, dist_dir):
25 | """Build npm dist file(s) from a package"""
26 | # Clean the dist folder of existing npm tarballs
27 | os.makedirs(dist_dir, exist_ok=True)
28 | dest = Path(dist_dir)
29 | for pkg in glob(f"{dist_dir}/*.tgz"):
30 | os.remove(pkg)
31 |
32 | if osp.isdir(package):
33 | tarball = osp.join(package, util.run("npm pack", cwd=package).split("\n")[-1])
34 | else:
35 | tarball = package
36 |
37 | data = extract_package(tarball)
38 |
39 | # Move the tarball into the dist folder if public
40 | if not data.get("private", False):
41 | shutil.move(str(tarball), str(dest))
42 | elif osp.isdir(package):
43 | os.remove(tarball)
44 |
45 | if not osp.isdir(package):
46 | return
47 |
48 | if "workspaces" in data:
49 | paths = []
50 | for path in _get_workspace_packages(data):
51 | package_json = path / "package.json"
52 | data = json.loads(package_json.read_text(encoding="utf-8"))
53 | if data.get("private", False):
54 | continue
55 | paths.append(str(osp.abspath(path)).replace(os.sep, "/"))
56 | if paths:
57 | util.run(f"npm pack {' '.join(paths)}", cwd=dest, quiet=True)
58 | else:
59 | util.log(
60 | "The NPM package defines 'workspaces' that does not contain any public package; this may be a mistake."
61 | )
62 |
63 |
64 | def extract_dist(dist_dir, target, repo=""):
65 | """Extract dist files from a dist_dir into a target dir
66 |
67 |
68 | If `repo` is provided, check that the repository URL is ending by it.
69 | """
70 | names = []
71 | paths = sorted(glob(f"{dist_dir}/*.tgz"))
72 | util.log(f"Extracting {len(paths)} packages...")
73 |
74 | for package in paths:
75 | path = Path(package)
76 |
77 | data = extract_package(path)
78 | name = data["name"]
79 |
80 | if repo and os.name != "nt":
81 | data_repository = data.get("repository", {})
82 | if isinstance(data_repository, str):
83 | url = data_repository
84 | else:
85 | url = data_repository.get("url", "")
86 | if url.endswith(".git"):
87 | url = url[:-4]
88 | if not url.endswith(repo):
89 | msg = f"package.json for '{name}' does not define a 'repository.url' matching the cloned repository '{repo}'."
90 | raise ValueError(msg)
91 |
92 | # Skip if it is a private package
93 | if data.get("private", False): # pragma: no cover
94 | util.log(f"Skipping private package {name}")
95 | continue
96 |
97 | names.append(name)
98 |
99 | pkg_dir = target / name
100 | if not pkg_dir.parent.exists():
101 | os.makedirs(pkg_dir.parent)
102 |
103 | tar = tarfile.open(path)
104 | tar.extractall(target) # noqa: S202
105 | tar.close()
106 |
107 | if "main" in data:
108 | main = osp.join(target, "package", data["main"])
109 | if not osp.exists(main):
110 | msg = f"{name} is missing 'main' file {data['main']}"
111 | raise ValueError(msg)
112 |
113 | shutil.move(str(target / "package"), str(pkg_dir))
114 |
115 | return names
116 |
117 |
118 | def check_dist(dist_dir, install_options, repo):
119 | """Check npm dist file(s) in a dist dir"""
120 | repo = repo or util.get_repo()
121 |
122 | with TemporaryDirectory() as td:
123 | util.run("npm init -y", cwd=td, quiet=True)
124 | names = []
125 | staging = Path(td) / "staging"
126 |
127 | names = extract_dist(dist_dir, staging, repo)
128 |
129 | install_str = " ".join(f"./staging/{name}" for name in names)
130 |
131 | util.run(f"npm install {install_options} {install_str}", cwd=td, quiet=True)
132 |
133 |
134 | def extract_package(path):
135 | """Get the package json info from the tarball"""
136 | fid = tarfile.open(path)
137 | fidfile = fid.extractfile("package/package.json")
138 | assert fidfile is not None
139 | data = fidfile.read()
140 | fidfile.close()
141 | data = json.loads(data.decode("utf-8"))
142 | fid.close()
143 | return data
144 |
145 |
146 | def handle_npm_config(npm_token):
147 | """Handle npm_config"""
148 | npmrc = Path("~/.npmrc").expanduser()
149 | registry = os.environ.get("NPM_REGISTRY", "https://registry.npmjs.org/")
150 | reg_entry = text = f"registry={registry}"
151 | auth_entry = ""
152 | if npm_token:
153 | short_reg = registry.replace("https://", "//")
154 | short_reg = short_reg.replace("http://", "//")
155 | auth_entry = f"{short_reg}:_authToken={npm_token}"
156 |
157 | # Handle existing config
158 | if npmrc.exists():
159 | text = npmrc.read_text(encoding="utf-8")
160 | if reg_entry in text:
161 | reg_entry = ""
162 | if auth_entry in text:
163 | auth_entry = ""
164 |
165 | text += f"\n{reg_entry}\n{auth_entry}"
166 |
167 | if os.environ.get(util.GH_ID_TOKEN_TOKEN_VAR, ""):
168 | util.log("Turning on NPM provenance as id-token permission is set.")
169 | # See documentation https://docs.npmjs.com/generating-provenance-statements
170 | # Also https://github.blog/2023-04-19-introducing-npm-package-provenance/
171 | text += "\nprovenance=true"
172 |
173 | text = text.strip() + "\n"
174 | util.log(f"writing npm config to {npmrc}")
175 | npmrc.write_text(text, encoding="utf-8")
176 |
177 |
178 | def get_package_versions(version):
179 | """Get the formatted list of npm package names and versions"""
180 | message = ""
181 | data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
182 | npm_version = data.get("version", "")
183 | if npm_version != version:
184 | message += f"\nPython version: {version}"
185 | message += f'\nnpm version: {data["name"]}: {npm_version}'
186 | if "workspaces" in data:
187 | message += "\nnpm workspace versions:"
188 | for path in _get_workspace_packages(data):
189 | text = path.joinpath("package.json").read_text(encoding="utf-8")
190 | data = json.loads(text)
191 | message += f'\n{data["name"]}: {data.get("version", "")}'
192 | return message
193 |
194 |
195 | def tag_workspace_packages():
196 | """Generate tags for npm workspace packages"""
197 | if not PACKAGE_JSON.exists():
198 | return
199 |
200 | data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
201 | tags = util.run("git tag", quiet=True).splitlines()
202 | if "workspaces" not in data:
203 | return
204 |
205 | skipped = []
206 | for path in _get_workspace_packages(data):
207 | sub_package_json = path / "package.json"
208 | sub_data = json.loads(sub_package_json.read_text(encoding="utf-8"))
209 | # Don't tag package without version or private
210 | if not sub_data.get("version", "") or sub_data.get("private", False):
211 | continue
212 | tag_name = f"{sub_data['name']}@{sub_data['version']}"
213 | if tag_name in tags:
214 | skipped.append(tag_name)
215 | else:
216 | util.run(f"git tag {tag_name}")
217 | if skipped:
218 | print(f"\nSkipped existing tags:\n{skipped}\n")
219 |
220 |
221 | def _get_workspace_packages(data):
222 | """Get the workspace package paths for a package given package data"""
223 | if isinstance(data["workspaces"], dict):
224 | patterns = []
225 | for value in data["workspaces"].values():
226 | patterns.extend(value)
227 | else:
228 | patterns = data["workspaces"]
229 |
230 | paths = []
231 | for pattern in patterns:
232 | for path in glob(pattern, recursive=True):
233 | sub_package = Path(path)
234 | if not sub_package.is_dir():
235 | continue
236 | sub_package_json = sub_package / "package.json"
237 | if not sub_package_json.exists():
238 | continue
239 | paths.append(sub_package)
240 |
241 | return sorted(paths)
242 |
--------------------------------------------------------------------------------
/jupyter_releaser/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/jupyter_releaser/py.typed
--------------------------------------------------------------------------------
/jupyter_releaser/python.py:
--------------------------------------------------------------------------------
1 | """Python-related utilities."""
2 | # Copyright (c) Jupyter Development Team.
3 | # Distributed under the terms of the Modified BSD License.
4 | import atexit
5 | import json
6 | import os
7 | import os.path as osp
8 | import re
9 | import shlex
10 | from glob import glob
11 | from io import BytesIO
12 | from pathlib import Path
13 | from subprocess import PIPE, CalledProcessError, Popen
14 | from tempfile import TemporaryDirectory
15 | from typing import cast
16 |
17 | import requests
18 |
19 | from jupyter_releaser import util
20 |
21 | PYPROJECT = util.PYPROJECT
22 | SETUP_PY = util.SETUP_PY
23 |
24 | PYPI_GH_API_TOKEN_URL = "https://pypi.org/_/oidc/github/mint-token" # noqa: S105
25 |
26 |
27 | def build_dist(dist_dir, clean=True):
28 | """Build the python dist files into a dist folder"""
29 | # Clean the dist folder of existing npm tarballs
30 | os.makedirs(dist_dir, exist_ok=True)
31 | dest = Path(dist_dir)
32 | if clean:
33 | for pkg in glob(f"{dest}/*.gz") + glob(f"{dest}/*.whl"):
34 | os.remove(pkg)
35 |
36 | util.run(f"pipx run build --outdir {dest} .", quiet=True, show_cwd=True)
37 |
38 |
39 | def check_dist(
40 | dist_file,
41 | test_cmd="",
42 | python_imports=None,
43 | check_cmd="pipx run twine check --strict {dist_file}",
44 | extra_check_cmds=None,
45 | resource_paths=None,
46 | ):
47 | """Check a Python package locally (not as a cli)"""
48 | resource_paths = resource_paths or []
49 | dist_file = util.normalize_path(dist_file)
50 | dist_dir = os.path.dirname(dist_file) # used for check cmds.
51 |
52 | for cmd in [check_cmd, *list(extra_check_cmds or [])]:
53 | util.run(cmd.format(**locals()))
54 |
55 | test_commands = []
56 |
57 | if test_cmd:
58 | test_commands.append(test_cmd)
59 | elif python_imports is not None:
60 | test_commands.extend([f'python -c "import {name}"' for name in python_imports])
61 | else:
62 | # Get the package name from the dist file name
63 | match = re.match(r"(\S+)-\d", osp.basename(dist_file))
64 | assert match is not None
65 | name = match.groups()[0]
66 | name = name.replace("-", "_")
67 | test_commands.append(f'python -c "import {name}"')
68 |
69 | # Create venvs to install dist file
70 | # run the test command in the venv
71 | with TemporaryDirectory() as td:
72 | env_path = util.normalize_path(osp.abspath(td))
73 | if os.name == "nt": # pragma: no cover # noqa: SIM108
74 | bin_path = f"{env_path}/Scripts/"
75 | else:
76 | bin_path = f"{env_path}/bin"
77 |
78 | # Create the virtual env, upgrade pip,
79 | # install, and run test command
80 | util.run(f"python -m venv {env_path}")
81 | util.run(f"{bin_path}/python -m pip install -q -U pip")
82 | util.run(f"{bin_path}/pip install -q {dist_file}")
83 | try:
84 | for cmd in test_commands:
85 | util.run(f"{bin_path}/{cmd}")
86 | except CalledProcessError as e:
87 | if test_cmd == "":
88 | util.log(
89 | 'You may need to set "check_imports" to appropriate Python package names in the config file.'
90 | )
91 | raise e
92 | for resource_path in resource_paths:
93 | name, _, _ = resource_path.partition("/")
94 | test_file = Path(td) / "test_path.py"
95 | test_text = f"""
96 | from importlib.metadata import PackagePath, files
97 | assert PackagePath('{resource_path}') in files('{name}')
98 | """
99 | test_file.write_text(test_text, encoding="utf-8")
100 | test_file = util.normalize_path(test_file)
101 | cmd = f"{bin_path}/python {test_file}"
102 | util.run(cmd)
103 |
104 |
105 | def fetch_pypi_api_token() -> "str":
106 | """Fetch the PyPI API token for trusted publishers
107 |
108 | This implements the manual steps described in https://docs.pypi.org/trusted-publishers/using-a-publisher/
109 | as of June 19th, 2023.
110 |
111 | It returns an empty string if it fails.
112 | """
113 | util.log("Fetching PyPI OIDC token...")
114 |
115 | url = os.environ.get(util.GH_ID_TOKEN_URL_VAR, "")
116 | auth = os.environ.get(util.GH_ID_TOKEN_TOKEN_VAR, "")
117 | if not url or not auth:
118 | util.log(
119 | "Please verify that you have granted `id-token: write` permission to the publish workflow."
120 | )
121 | return ""
122 |
123 | headers = {"Authorization": f"bearer {auth}", "Accept": "application/octet-stream"}
124 |
125 | sink = BytesIO()
126 | with requests.get(f"{url}&audience=pypi", headers=headers, stream=True, timeout=60) as r:
127 | r.raise_for_status()
128 | for chunk in r.iter_content(chunk_size=8192):
129 | sink.write(chunk)
130 | sink.seek(0)
131 | oidc_token = json.loads(sink.read().decode("utf-8")).get("value", "")
132 |
133 | if not oidc_token:
134 | util.log("Failed to fetch the OIDC token from PyPI.")
135 | return ""
136 |
137 | util.log("Fetching PyPI API token...")
138 | sink = BytesIO()
139 | with requests.post(PYPI_GH_API_TOKEN_URL, json={"token": oidc_token}, timeout=10) as r:
140 | r.raise_for_status()
141 | for chunk in r.iter_content(chunk_size=8192):
142 | sink.write(chunk)
143 | sink.seek(0)
144 | api_token = json.loads(sink.read().decode("utf-8")).get("token", "")
145 |
146 | return cast(str, api_token)
147 |
148 |
149 | def get_pypi_token(release_url, python_package):
150 | """Get the PyPI token
151 |
152 | Note: Do not print the token in CI since it will not be sanitized
153 | if it comes from the PYPI_TOKEN_MAP"""
154 | trusted_token = os.environ.get(util.GH_ID_TOKEN_TOKEN_VAR, "")
155 |
156 | if trusted_token:
157 | return fetch_pypi_api_token()
158 |
159 | twine_pwd = os.environ.get("PYPI_TOKEN", "")
160 | pypi_token_map = os.environ.get("PYPI_TOKEN_MAP", "").replace(r"\n", "\n")
161 | if pypi_token_map and release_url:
162 | parts = (
163 | release_url.replace(util.get_mock_github_url() + "/", "")
164 | .replace("https://github.com/", "")
165 | .split("/")
166 | )
167 | repo_name = f"{parts[0]}/{parts[1]}"
168 | if python_package != ".":
169 | repo_name += f"/{python_package}"
170 | util.log(f"Looking for PYPI token for {repo_name} in token map")
171 | for line in pypi_token_map.splitlines():
172 | name, _, token = line.partition(",")
173 | if name == repo_name:
174 | twine_pwd = token.strip()
175 | util.log(f"Found PYPI token in map ending in {twine_pwd[-5:]}")
176 | elif twine_pwd:
177 | util.log("Using PYPI token from PYPI_TOKEN")
178 | else:
179 | util.log("PYPI token not found")
180 |
181 | return twine_pwd
182 |
183 |
184 | def start_local_pypi():
185 | """Start a local PyPI server"""
186 | temp_dir = TemporaryDirectory()
187 | cmd = f"pypi-server run -p 8081 -P . -a . -o -v {temp_dir.name}"
188 | proc = Popen(shlex.split(cmd), stdout=PIPE) # noqa: S603
189 | # Wait for the server to start
190 | while True:
191 | assert proc.stdout is not None
192 | line = proc.stdout.readline().decode("utf-8").strip()
193 | util.log(line)
194 | if "Listening on" in line:
195 | break
196 | atexit.register(proc.kill)
197 | atexit.register(temp_dir.cleanup)
198 |
--------------------------------------------------------------------------------
/jupyter_releaser/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Jupyter Releaser Metadata",
3 | "version": "0.1.0",
4 | "description": "Jupyter Releaser configuration metadata",
5 | "properties": {
6 | "skip": {
7 | "title": "Skip Steps",
8 | "description": "A list of steps to skip in actions",
9 | "type": "array",
10 | "items": {
11 | "type": "string"
12 | }
13 | },
14 | "options": {
15 | "title": "Overrides for default options",
16 | "description": "Overrides for cli option names",
17 | "additionalProperties": {
18 | "anyOf": [
19 | { "type": "string" },
20 | {
21 | "type": "array",
22 | "items": {
23 | "type": "string"
24 | }
25 | }
26 | ]
27 | }
28 | },
29 | "hooks": {
30 | "title": "Action Step Hooks",
31 | "description": "Hooks to run before or after action steps",
32 | "patternProperties": {
33 | "^(before|after)-.*$": {
34 | "anyOf": [
35 | { "type": "string" },
36 | {
37 | "type": "array",
38 | "items": {
39 | "type": "string"
40 | }
41 | }
42 | ]
43 | }
44 | }
45 | }
46 | },
47 | "additionalProperties": false,
48 | "type": "object"
49 | }
50 |
--------------------------------------------------------------------------------
/jupyter_releaser/tee.py:
--------------------------------------------------------------------------------
1 | """tee like run implementation."""
2 | # This file is a modified version of https://github.com/pycontribs/subprocess-tee/blob/daffcbbf49fc5a2c7f3eaf75551f08fac0b9b63d/src/subprocess_tee/__init__.py
3 | #
4 | # It is licensed under the following license:
5 | #
6 | # The MIT License
7 | # Copyright (c) 2020 Sorin Sbarnea
8 | # Permission is hereby granted, free of charge, to any person obtaining a copy
9 | # of this software and associated documentation files (the "Software"), to deal
10 | # in the Software without restriction, including without limitation the rights
11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | # copies of the Software, and to permit persons to whom the Software is
13 | # furnished to do so, subject to the following conditions:
14 | # The above copyright notice and this permission notice shall be included in
15 | # all copies or substantial portions of the Software.
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | # THE SOFTWARE.
23 | import asyncio
24 | import atexit
25 | import os
26 | import platform
27 | import subprocess
28 | import sys
29 | from asyncio import StreamReader
30 | from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
31 |
32 | if TYPE_CHECKING:
33 | CompletedProcess = subprocess.CompletedProcess[Any] # pylint: disable=E1136
34 | else:
35 | CompletedProcess = subprocess.CompletedProcess
36 |
37 | try:
38 | from shlex import join
39 | except ImportError:
40 | from subprocess import list2cmdline as join # type:ignore[assignment]
41 |
42 |
43 | STREAM_LIMIT = 2**23 # 8MB instead of default 64kb, override it if you need
44 |
45 |
46 | async def _read_stream(stream: StreamReader, callback: Callable[..., Any]) -> None:
47 | while True:
48 | line = await stream.readline()
49 | if line:
50 | callback(line)
51 | else:
52 | break
53 |
54 |
55 | async def _stream_subprocess(args: str, **kwargs: Any) -> CompletedProcess:
56 | platform_settings: Dict[str, Any] = {}
57 | if platform.system() == "Windows":
58 | platform_settings["env"] = os.environ
59 |
60 | # this part keeps behavior backwards compatible with subprocess.run
61 | tee = kwargs.get("tee", True)
62 | stdout = kwargs.get("stdout", sys.stdout)
63 | if stdout == subprocess.DEVNULL or not tee:
64 | stdout = open(os.devnull, "w") # noqa: SIM115
65 | stderr = kwargs.get("stderr", sys.stderr)
66 | if stderr == subprocess.DEVNULL or not tee:
67 | stderr = open(os.devnull, "w") # noqa: SIM115
68 |
69 | # We need to tell subprocess which shell to use when running shell-like
70 | # commands.
71 | # * SHELL is not always defined
72 | # * /bin/bash does not exit on alpine, /bin/sh seems bit more portable
73 | if "executable" not in kwargs and isinstance(args, str) and " " in args: # type:ignore[redundant-expr]
74 | platform_settings["executable"] = os.environ.get("SHELL", "/bin/sh")
75 |
76 | # pass kwargs we know to be supported
77 | for arg in ["cwd", "env"]:
78 | if arg in kwargs:
79 | platform_settings[arg] = kwargs[arg]
80 |
81 | # Some users are reporting that default (undocumented) limit 64k is too
82 | # low
83 | process = await asyncio.create_subprocess_shell(
84 | args,
85 | limit=STREAM_LIMIT,
86 | stdin=kwargs.get("stdin", False),
87 | stdout=asyncio.subprocess.PIPE,
88 | stderr=asyncio.subprocess.PIPE,
89 | **platform_settings,
90 | )
91 | out: List[str] = []
92 | err: List[str] = []
93 |
94 | def tee_func(line: bytes, sink: List[str], pipe: Optional[Any]) -> None: # noqa: ARG001
95 | line_str = line.decode("utf-8").rstrip()
96 | sink.append(line_str)
97 | if not kwargs.get("quiet", False):
98 | # This is modified from the default implementation since
99 | # we want all output to be interleved on the same stream
100 | print(line_str, file=sys.stderr)
101 |
102 | loop = asyncio.get_running_loop()
103 | tasks = []
104 | if process.stdout:
105 | tasks.append(
106 | loop.create_task(_read_stream(process.stdout, lambda li: tee_func(li, out, stdout)))
107 | )
108 | if process.stderr:
109 | tasks.append(
110 | loop.create_task(_read_stream(process.stderr, lambda li: tee_func(li, err, stderr)))
111 | )
112 |
113 | await asyncio.wait(set(tasks))
114 |
115 | # We need to be sure we keep the stdout/stderr output identical with
116 | # the ones procued by subprocess.run(), at least when in text mode.
117 | check = kwargs.get("check", False)
118 | stdout = None if check else ""
119 | stderr = None if check else ""
120 | if out:
121 | stdout = os.linesep.join(out) + os.linesep
122 | if err:
123 | stderr = os.linesep.join(err) + os.linesep
124 |
125 | return CompletedProcess(
126 | args=args,
127 | returncode=await process.wait(),
128 | stdout=stdout,
129 | stderr=stderr,
130 | )
131 |
132 |
133 | def run(args: Union[str, List[str]], **kwargs: Any) -> CompletedProcess:
134 | """Drop-in replacement for subprocess.run that behaves like tee.
135 | Extra arguments added by our version:
136 | echo: False - Prints command before executing it.
137 | quiet: False - Avoid printing output
138 | show_cwd: False - Prints the current working directory.
139 | """
140 | if isinstance(args, str): # noqa: SIM108
141 | cmd = args
142 | else:
143 | # run was called with a list instead of a single item but asyncio
144 | # create_subprocess_shell requires command as a single string, so
145 | # we need to convert it to string
146 | cmd = join(args)
147 |
148 | check = kwargs.get("check", False)
149 |
150 | try:
151 | loop = asyncio.get_event_loop()
152 | except Exception:
153 | loop = asyncio.new_event_loop()
154 | asyncio.set_event_loop(loop)
155 | result = loop.run_until_complete(_stream_subprocess(cmd, **kwargs))
156 | atexit.register(loop.close)
157 |
158 | if check and result.returncode != 0:
159 | raise subprocess.CalledProcessError(
160 | result.returncode, cmd, output=result.stdout, stderr=result.stderr
161 | )
162 | return result
163 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling>=1.11"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "jupyter_releaser"
7 | description = "Jupyter Releaser for Python and/or npm packages."
8 | license = {file = "LICENSE"}
9 | authors = [{name = "Jupyter Development Team", email = "jupyter@googlegroups.com"}]
10 | keywords = ["ipython", "jupyter"]
11 | classifiers = [
12 | "Intended Audience :: Developers",
13 | "Intended Audience :: System Administrators",
14 | "Intended Audience :: Science/Research",
15 | "License :: OSI Approved :: BSD License",
16 | "Programming Language :: Python",
17 | "Programming Language :: Python :: 3.9",
18 | "Programming Language :: Python :: 3.10",
19 | "Programming Language :: Python :: 3.11",
20 | "Programming Language :: Python :: 3.12",
21 | "Programming Language :: Python :: 3.13",
22 | ]
23 | urls = {Homepage = "https://jupyter.org"}
24 | requires-python = ">=3.9"
25 | dynamic = ["version"]
26 | dependencies = [
27 | "click<8.2.0",
28 | "ghapi<=1.0.4",
29 | "github-activity~=0.2",
30 | "importlib_resources",
31 | "jsonschema>=3.0.1",
32 | "mdformat",
33 | "packaging",
34 | "pkginfo",
35 | "pypiserver==2.2.0; python_version < '3.13'",
36 | "pypiserver; python_version >= '3.13'",
37 | "pipx",
38 | "requests",
39 | "requests_cache",
40 | "toml~=0.10",
41 | ]
42 |
43 | [project.readme]
44 | file = "README.md"
45 | content-type = "text/markdown"
46 |
47 | [project.optional-dependencies]
48 | docs = [
49 | "sphinx",
50 | "sphinx-autobuild",
51 | "sphinx-copybutton",
52 | "pip",
53 | "myst-parser",
54 | "pydata_sphinx_theme",
55 | "sphinxcontrib_spelling",
56 | "numpydoc",
57 | "sphinx-click",
58 | ]
59 | test = [
60 | "fastapi",
61 | "pre-commit",
62 | "pytest>=7.0",
63 | "pytest-mock",
64 | "pytest-xdist[psutil]",
65 | "uvicorn",
66 | "ruamel.yaml"
67 | ]
68 |
69 | [project.scripts]
70 | jupyter-releaser = "jupyter_releaser.cli:main"
71 |
72 | [tool.hatch.version]
73 | path = "jupyter_releaser/__init__.py"
74 | validate-bump = false
75 |
76 | [tool.hatch.envs.docs]
77 | features = ["docs"]
78 | [tool.hatch.envs.docs.scripts]
79 | build = "sphinx-build -W -b html docs/source docs/build/html"
80 | serve = "python -m http.server --directory docs/build/html"
81 | watch = "sphinx-autobuild -W -b html docs/source docs/build/html --host 0.0.0.0"
82 |
83 | [tool.hatch.envs.test]
84 | features = ["test"]
85 | [tool.hatch.envs.test.scripts]
86 | test = "python -m pytest -vv {args}"
87 | nowarn = "test -W default {args}"
88 |
89 | [tool.hatch.envs.cov]
90 | features = ["test"]
91 | dependencies = ["coverage[toml]", "pytest-cov"]
92 | [tool.hatch.envs.cov.scripts]
93 | test = "python -m pytest -vv --cov jupyter_releaser --cov-branch --cov-report term-missing:skip-covered {args}"
94 | nowarn = "test -W default {args}"
95 |
96 | [tool.hatch.envs.lint]
97 | detached = true
98 | dependencies = ["pre-commit"]
99 | [tool.hatch.envs.lint.scripts]
100 | build = "pre-commit run --all-files ruff"
101 |
102 | [tool.hatch.envs.typing]
103 | dependencies = [ "pre-commit"]
104 | detached = true
105 | [tool.hatch.envs.typing.scripts]
106 | test = "pre-commit run --all-files --hook-stage manual mypy"
107 |
108 | [tool.jupyter-releaser.hooks]
109 | after-populate-release = "bash ./.github/scripts/bump_tag.sh"
110 |
111 | [tool.jupyter-releaser.options]
112 | post-version-spec = "dev"
113 |
114 | [tool.pytest.ini_options]
115 | minversion = "6.0"
116 | xfail_strict = true
117 | log_cli_level = "info"
118 | addopts = [
119 | "-ra", "--durations=10", "--color=yes", "--doctest-modules",
120 | "--showlocals", "--strict-markers", "--strict-config",
121 | "-p", "no:pastebin", "-p", "no:nose"
122 | ]
123 | testpaths = [
124 | "tests/"
125 | ]
126 | filterwarnings= [
127 | # Fail on warnings
128 | "error",
129 | "ignore:Using deprecated setup.py invocation:UserWarning",
130 | "module:Neither GITHUB_TOKEN nor GITHUB_JWT_TOKEN found:UserWarning",
131 | "module:datetime.datetime.utc:DeprecationWarning",
132 | "ignore:(?s).*Pyarrow will become a required dependency of pandas:DeprecationWarning", # pandas pyarrow (pandas<3.0),
133 | ]
134 |
135 | [tool.coverage.report]
136 | exclude_lines = [
137 | "pragma: no cover",
138 | "def __repr__",
139 | "if self.debug:",
140 | "if settings.DEBUG",
141 | "raise AssertionError",
142 | "raise NotImplementedError",
143 | "if 0:",
144 | "if __name__ == .__main__.:",
145 | "class .*\bProtocol\\):",
146 | "@(abc\\.)?abstractmethod",
147 | ]
148 |
149 | [tool.coverage.run]
150 | relative_files = true
151 | source = ["jupyter_releaser"]
152 |
153 | [tool.mypy]
154 | files = "jupyter_releaser"
155 | python_version = "3.8"
156 | strict = true
157 | disable_error_code = [ "no-untyped-call", "no-untyped-def"]
158 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
159 | warn_unreachable = true
160 | exclude = ["tests"]
161 |
162 | [tool.ruff]
163 | target-version = "py38"
164 | line-length = 100
165 |
166 | [tool.ruff.lint]
167 | extend-select = [
168 | "B", # flake8-bugbear
169 | "I", # isort
170 | "ARG", # flake8-unused-arguments
171 | "C4", # flake8-comprehensions
172 | "EM", # flake8-errmsg
173 | "ICN", # flake8-import-conventions
174 | "G", # flake8-logging-format
175 | "PGH", # pygrep-hooks
176 | "PIE", # flake8-pie
177 | "PL", # pylint
178 | #"PTH", # flake8-use-pathlib
179 | "PT", # flake8-pytest-style
180 | "RET", # flake8-return
181 | "RUF", # Ruff-specific
182 | "SIM", # flake8-simplify
183 | "T20", # flake8-print
184 | "UP", # pyupgrade
185 | "YTT", # flake8-2020
186 | "EXE", # flake8-executable
187 | "PYI", # flake8-pyi
188 | "S", # flake8-bandit
189 | ]
190 | ignore = [
191 | "PLR", # Design related pylint codes
192 | "SIM105", # Use `contextlib.suppress(...)`
193 | "T201", # `print` found
194 | "S101", # Use of `assert` detected
195 | ]
196 | unfixable = [
197 | # Don't touch print statements
198 | "T201",
199 | # Don't touch noqa lines
200 | "RUF100",
201 | ]
202 |
203 | [tool.ruff.lint.per-file-ignores]
204 | # B011 Do not call assert False since python -O removes these calls
205 | # F841 local variable 'foo' is assigned to but never used
206 | # C408 Unnecessary `dict` call
207 | # E402 Module level import not at top of file
208 | # T201 `print` found
209 | # B007 Loop control variable `i` not used within the loop body.
210 | # N802 Function name `assertIn` should be lowercase
211 | # S105 Possible hardcoded password
212 | "tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "S105", "S106", "ARG", "PTH", "PT004", "PT011"]
213 | "jupyter_releaser/mock_github.py" = ["ARG"]
214 |
215 | [tool.interrogate]
216 | ignore-init-module=true
217 | ignore-private=true
218 | ignore-semiprivate=true
219 | ignore-property-decorators=true
220 | ignore-nested-functions=true
221 | ignore-nested-classes=true
222 | fail-under=100
223 | exclude = ["tests"]
224 |
225 | [tool.repo-review]
226 | ignore = ["GH102"]
227 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jupyter-server/jupyter_releaser/6accaa3c07b69acaa1e14e00ba138133d8cbe879/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Jupyter Development Team.
2 | # Distributed under the terms of the Modified BSD License.
3 | import json
4 | import os
5 | import os.path as osp
6 | import tempfile
7 | import time
8 | import uuid
9 | from pathlib import Path
10 |
11 | import pytest
12 | from click.testing import CliRunner
13 | from ghapi.core import GhApi
14 |
15 | from jupyter_releaser import cli, util
16 | from jupyter_releaser.util import ensure_mock_github, run
17 | from tests import util as testutil
18 |
19 |
20 | @pytest.fixture(autouse=True)
21 | def github_port(worker_id):
22 | # The worker id will be of the form "gw123" unless xdist is disabled,
23 | # in which case it will be "master".
24 | if worker_id == "master":
25 | return
26 | os.environ["MOCK_GITHUB_PORT"] = str(8000 + int(worker_id[2:]))
27 |
28 |
29 | @pytest.fixture(autouse=True)
30 | def mock_env(mocker):
31 | """Clear unwanted environment variables"""
32 | # Anything that starts with RH_ or GITHUB_ or PIP
33 | prefixes = ["GITHUB_", "RH_", "PIP_"]
34 | env = os.environ.copy()
35 | for key in list(env):
36 | for prefix in prefixes:
37 | if key.startswith(prefix):
38 | del env[key]
39 |
40 | mocker.patch.dict(os.environ, env, clear=True)
41 | return
42 |
43 |
44 | @pytest.fixture()
45 | def git_repo(tmp_path):
46 | prev_dir = os.getcwd()
47 | os.chdir(tmp_path)
48 |
49 | run("git init")
50 | run("git config user.name snuffy")
51 | run("git config user.email snuffy@sesame.com")
52 |
53 | run("git checkout -b foo")
54 | gitignore = tmp_path / ".gitignore"
55 | gitignore.write_text(f"dist/*\nbuild/*\n{util.CHECKOUT_NAME}\n", encoding="utf-8")
56 |
57 | changelog = tmp_path / "CHANGELOG.md"
58 | changelog.write_text(testutil.CHANGELOG_TEMPLATE, encoding="utf-8")
59 |
60 | config = Path(util.JUPYTER_RELEASER_CONFIG)
61 | config.write_text(testutil.TOML_CONFIG, encoding="utf-8")
62 |
63 | run("git add .")
64 | run('git commit -m "foo"')
65 | run(f"git remote add origin {util.normalize_path(tmp_path)}")
66 | run("git push origin foo")
67 | run("git remote set-head origin foo")
68 | run("git checkout -b bar foo")
69 | run("git fetch origin")
70 | yield tmp_path
71 | os.chdir(prev_dir)
72 |
73 |
74 | @pytest.fixture()
75 | def py_package(git_repo):
76 | return testutil.create_python_package(git_repo)
77 |
78 |
79 | @pytest.fixture()
80 | def py_multipackage(git_repo):
81 | return testutil.create_python_package(git_repo, multi=True)
82 |
83 |
84 | @pytest.fixture()
85 | def py_package_different_names(git_repo):
86 | return testutil.create_python_package(git_repo, not_matching_name=True)
87 |
88 |
89 | @pytest.fixture()
90 | def npm_package(git_repo):
91 | return testutil.create_npm_package(git_repo)
92 |
93 |
94 | @pytest.fixture()
95 | def workspace_package(npm_package):
96 | pkg_file = npm_package / "package.json"
97 | data = json.loads(pkg_file.read_text(encoding="utf-8"))
98 | data["workspaces"] = dict(packages=["packages/*"])
99 | data["private"] = True
100 | pkg_file.write_text(json.dumps(data), encoding="utf-8")
101 |
102 | prev_dir = Path(os.getcwd())
103 | for name in ["foo", "bar", "baz"]:
104 | new_dir = prev_dir / "packages" / name
105 | os.makedirs(new_dir)
106 | os.chdir(new_dir)
107 | run("npm init -y", quiet=True)
108 | index = new_dir / "index.js"
109 | index.write_text('console.log("hello")', encoding="utf-8")
110 | if name == "foo":
111 | pkg_json = new_dir / "package.json"
112 | sub_data = json.loads(pkg_json.read_text(encoding="utf-8"))
113 | sub_data["dependencies"] = dict(bar="*")
114 | sub_data["main"] = "index.js"
115 | sub_data["repository"] = dict(url=str(npm_package))
116 | pkg_json.write_text(json.dumps(sub_data), encoding="utf-8")
117 | elif name == "baz":
118 | pkg_json = new_dir / "package.json"
119 | sub_data = json.loads(pkg_json.read_text(encoding="utf-8"))
120 | sub_data["dependencies"] = dict(foo="*")
121 | sub_data["main"] = "index.js"
122 | sub_data["repository"] = dict(url=str(npm_package))
123 | pkg_json.write_text(json.dumps(sub_data), encoding="utf-8")
124 | elif name == "bar":
125 | pkg_json = new_dir / "package.json"
126 | sub_data = json.loads(pkg_json.read_text(encoding="utf-8"))
127 | sub_data["repository"] = dict(url=str(npm_package))
128 | pkg_json.write_text(json.dumps(sub_data), encoding="utf-8")
129 | os.chdir(prev_dir)
130 | util.run("git add .")
131 | util.run('git commit -a -m "Add workspaces"')
132 | return npm_package
133 |
134 |
135 | @pytest.fixture()
136 | def py_dist(py_package, runner, mocker, build_mock, git_prep):
137 | changelog_entry = testutil.mock_changelog_entry(py_package, runner, mocker)
138 |
139 | # Create the dist files
140 | util.run("pipx run build .", cwd=util.CHECKOUT_NAME, quiet=True)
141 |
142 | # Finalize the release
143 | runner(["tag-release"])
144 |
145 | return py_package
146 |
147 |
148 | @pytest.fixture()
149 | def npm_dist(workspace_package, runner, mocker, git_prep):
150 | changelog_entry = testutil.mock_changelog_entry(workspace_package, runner, mocker)
151 |
152 | # Create the dist files
153 | runner(["build-npm"])
154 |
155 | # Finalize the release
156 | runner(["tag-release"])
157 |
158 | return workspace_package
159 |
160 |
161 | @pytest.fixture()
162 | def runner():
163 | cli_runner = CliRunner()
164 |
165 | def run(*args, **kwargs):
166 | result = cli_runner.invoke(cli.main, *args, **kwargs)
167 | if result.exit_code != 0:
168 | if result.stderr_bytes:
169 | print("Captured stderr\n", result.stderr, "\n\n")
170 | print("Captured stdout\n", result.stdout, "\n\n")
171 | assert result.exception is not None
172 | raise result.exception
173 |
174 | return result
175 |
176 | return run
177 |
178 |
179 | @pytest.fixture()
180 | def git_prep(runner, git_repo):
181 | runner(["prep-git", "--git-url", git_repo])
182 |
183 |
184 | @pytest.fixture()
185 | def build_mock(mocker):
186 | orig_run = util.run
187 |
188 | def wrapped(cmd, **kwargs):
189 | if cmd == "pipx run build .":
190 | if osp.exists(util.CHECKOUT_NAME):
191 | dist_dir = Path(f"{util.CHECKOUT_NAME}/dist")
192 | else:
193 | dist_dir = Path("dist")
194 | os.makedirs(dist_dir, exist_ok=True)
195 | Path(f"{dist_dir}/foo-0.0.2a0.tar.gz").write_text("hello", encoding="utf-8")
196 | Path(f"{dist_dir}/foo-0.0.2a0-py3-none-any.whl").write_text("hello", encoding="utf-8")
197 | return ""
198 | return orig_run(cmd, **kwargs)
199 |
200 | mock_run = mocker.patch("jupyter_releaser.util.run", wraps=wrapped)
201 |
202 |
203 | @pytest.fixture()
204 | def mock_github():
205 | proc = ensure_mock_github()
206 | yield proc
207 |
208 | if proc:
209 | proc.kill()
210 | proc.wait()
211 |
212 |
213 | @pytest.fixture()
214 | def release_metadata():
215 | return dict(
216 | [(k, v) for k, v in testutil.BASE_RELEASE_METADATA.items() if k != "version"]
217 | + [("version", uuid.uuid4().hex)]
218 | )
219 |
220 |
221 | @pytest.fixture()
222 | def draft_release(mock_github, release_metadata):
223 | gh = GhApi(owner="foo", repo="bar")
224 | data = release_metadata
225 | tag = "v" + data["version"]
226 |
227 | with tempfile.TemporaryDirectory() as d:
228 | metadata_path = Path(d) / "metadata.json"
229 | with open(metadata_path, "w") as fid:
230 | json.dump(data, fid)
231 |
232 | # Ensure this is the latest release.
233 | time.sleep(1)
234 | release = gh.create_release(tag, "bar", tag, "hi", True, True, files=[metadata_path])
235 | yield release.html_url
236 | try:
237 | gh.repos.delete_release(release.id)
238 | except Exception as e:
239 | print(e)
240 |
--------------------------------------------------------------------------------
/tests/test_changelog.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Jupyter Development Team.
2 | # Distributed under the terms of the Modified BSD License.
3 |
4 | import os
5 | import subprocess
6 |
7 | import pytest
8 | from ghapi.core import GhApi
9 |
10 | from jupyter_releaser.changelog import (
11 | END_MARKER,
12 | END_SILENT_MARKER,
13 | START_MARKER,
14 | START_SILENT_MARKER,
15 | remove_placeholder_entries,
16 | update_changelog,
17 | )
18 | from jupyter_releaser.util import release_for_url
19 | from tests import util as testutil
20 |
21 |
22 | @pytest.fixture()
23 | def module_template():
24 | return testutil.PY_MODULE_TEMPLATE
25 |
26 |
27 | @pytest.fixture()
28 | def mock_py_package(tmp_path, module_template):
29 | pyproject = tmp_path / "pyproject.toml"
30 | pyproject.write_text(testutil.pyproject_template(), encoding="utf-8")
31 |
32 | foopy = tmp_path / "foo.py"
33 | foopy.write_text(module_template, encoding="utf-8")
34 |
35 |
36 | def test_update_changelog(tmp_path, mock_py_package):
37 | changelog = tmp_path / "CHANGELOG.md"
38 | changelog.write_text(testutil.CHANGELOG_TEMPLATE, encoding="utf-8")
39 |
40 | os.chdir(tmp_path)
41 | update_changelog(str(changelog), testutil.CHANGELOG_ENTRY)
42 |
43 | new_changelog = changelog.read_text(encoding="utf-8")
44 |
45 | assert f"{START_MARKER}\n{testutil.CHANGELOG_ENTRY}\n{END_MARKER}" in new_changelog
46 |
47 |
48 | def test_silent_update_changelog(tmp_path, mock_py_package):
49 | changelog = tmp_path / "CHANGELOG.md"
50 | changelog.write_text(testutil.CHANGELOG_TEMPLATE, encoding="utf-8")
51 | os.chdir(tmp_path)
52 | update_changelog(str(changelog), testutil.CHANGELOG_ENTRY, True)
53 |
54 | new_changelog = changelog.read_text(encoding="utf-8")
55 |
56 | assert (
57 | f"{START_MARKER}\n\n{START_SILENT_MARKER}\n\n## 0.0.1\n\n{END_SILENT_MARKER}\n\n{END_MARKER}"
58 | in new_changelog
59 | )
60 |
61 |
62 | def test_update_changelog_with_old_silent_entry(tmp_path, mock_py_package):
63 | changelog = tmp_path / "CHANGELOG.md"
64 | changelog.write_text(testutil.CHANGELOG_TEMPLATE, encoding="utf-8")
65 | os.chdir(tmp_path)
66 |
67 | # Update changelog for current version
68 | update_changelog(str(changelog), testutil.CHANGELOG_ENTRY, True)
69 | # Bump version
70 | subprocess.check_call(["pipx", "run", "hatch", "version", "patch"], cwd=tmp_path) # noqa: S603, S607
71 | # Update changelog for the new version
72 | update_changelog(str(changelog), testutil.CHANGELOG_ENTRY)
73 |
74 | new_changelog = changelog.read_text(encoding="utf-8")
75 |
76 | assert f"{START_SILENT_MARKER}\n\n## 0.0.1\n\n{END_SILENT_MARKER}" in new_changelog
77 | assert (
78 | f"{START_MARKER}\n\n{START_SILENT_MARKER}\n\n## 0.0.1\n\n{END_SILENT_MARKER}\n\n{END_MARKER}"
79 | not in new_changelog
80 | )
81 |
82 |
83 | @pytest.mark.parametrize("module_template", ['__version__ = "0.0.3"\n'])
84 | def test_silence_existing_changelog_entry(tmp_path, mock_py_package):
85 | changelog = tmp_path / "CHANGELOG.md"
86 | changelog.write_text(testutil.CHANGELOG_TEMPLATE, encoding="utf-8")
87 | os.chdir(tmp_path)
88 | update_changelog(str(changelog), testutil.CHANGELOG_ENTRY)
89 |
90 | new_changelog = changelog.read_text(encoding="utf-8")
91 | assert f"{START_MARKER}\n{testutil.CHANGELOG_ENTRY}\n{END_MARKER}" in new_changelog
92 |
93 | # Should silent the current entry
94 | update_changelog(str(changelog), testutil.CHANGELOG_ENTRY, True)
95 |
96 | new_changelog = changelog.read_text(encoding="utf-8")
97 | assert (
98 | f"{START_MARKER}\n\n{START_SILENT_MARKER}\n\n## 0.0.3\n\n{END_SILENT_MARKER}\n\n{END_MARKER}"
99 | in new_changelog
100 | )
101 | assert f"{START_MARKER}\n{testutil.CHANGELOG_ENTRY}\n{END_MARKER}" not in new_changelog
102 |
103 |
104 | @pytest.mark.parametrize(
105 | "release_metadata",
106 | [
107 | dict(
108 | [(k, v) for k, v in testutil.BASE_RELEASE_METADATA.items() if k != "version"]
109 | + [("version", "0.0.0")]
110 | )
111 | ],
112 | )
113 | def test_remove_placeholder_entries(tmp_path, release_metadata, draft_release):
114 | # Create changelog with silent placeholder
115 | changelog = tmp_path / "CHANGELOG.md"
116 | placeholder = (
117 | f"\n{START_SILENT_MARKER}\n\n## {release_metadata['version']}\n\n{END_SILENT_MARKER}\n"
118 | )
119 | changelog.write_text(testutil.CHANGELOG_TEMPLATE + placeholder, encoding="utf-8")
120 | os.chdir(tmp_path)
121 |
122 | # Publish the release (as it is a draft from `draft_release`)
123 | gh = GhApi(owner="foo", repo="bar")
124 | release = release_for_url(gh, draft_release)
125 | published_changelog = "Published body"
126 | gh.repos.update_release(
127 | release["id"],
128 | release["tag_name"],
129 | release["target_commitish"],
130 | release["name"],
131 | published_changelog,
132 | False,
133 | release["prerelease"],
134 | )
135 |
136 | remove_placeholder_entries("foo/bar", None, changelog, False)
137 |
138 | new_changelog = changelog.read_text(encoding="utf-8")
139 | assert placeholder not in new_changelog
140 | assert published_changelog in new_changelog
141 |
142 |
143 | @pytest.mark.parametrize(
144 | "release_metadata",
145 | [
146 | dict(
147 | [
148 | (k, v)
149 | for k, v in testutil.BASE_RELEASE_METADATA.items()
150 | if k not in ("version", "silent")
151 | ]
152 | + [("version", "0.0.0"), ("silent", True)]
153 | )
154 | ],
155 | )
156 | def test_dont_remove_placeholder_entries(tmp_path, release_metadata, draft_release):
157 | changelog = tmp_path / "CHANGELOG.md"
158 | placeholder = (
159 | f"\n{START_SILENT_MARKER}\n\n## {release_metadata['version']}\n\n{END_SILENT_MARKER}\n"
160 | )
161 | changelog.write_text(testutil.CHANGELOG_TEMPLATE + placeholder, encoding="utf-8")
162 | os.chdir(tmp_path)
163 |
164 | # Release is not published, so this is a no-op
165 | remove_placeholder_entries("foo/bar", None, changelog, False)
166 |
167 | new_changelog = changelog.read_text(encoding="utf-8")
168 | assert placeholder in new_changelog
169 | assert "hi" not in new_changelog
170 |
--------------------------------------------------------------------------------
/tests/test_functions.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Jupyter Development Team.
2 | # Distributed under the terms of the Modified BSD License.
3 | import json
4 | import os
5 | import shutil
6 | import time
7 | from pathlib import Path
8 |
9 | import toml
10 | from ghapi.core import GhApi
11 |
12 | from jupyter_releaser import changelog, npm, util
13 | from jupyter_releaser.util import run
14 | from tests import util as testutil
15 |
16 |
17 | def test_get_branch(git_repo):
18 | assert util.get_branch() == "bar"
19 | run("git checkout foo")
20 | assert util.get_branch() == "foo"
21 |
22 |
23 | def test_get_repo(git_repo, mocker):
24 | repo = f"{git_repo.parent.name}/{git_repo.name}"
25 | assert util.get_repo() == repo
26 |
27 |
28 | def test_get_version_pyproject_hatch(py_package):
29 | assert util.get_version() == "0.0.1"
30 | util.bump_version("0.0.2a0")
31 | assert util.get_version() == "0.0.2a0"
32 |
33 |
34 | def test_get_version_multipython(py_multipackage):
35 | prev_dir = os.getcwd()
36 | for package in py_multipackage:
37 | os.chdir(package["rel_path"])
38 | assert util.get_version() == "0.0.1"
39 | util.bump_version("0.0.2a0")
40 | assert util.get_version() == "0.0.2a0"
41 | os.chdir(prev_dir)
42 |
43 |
44 | def test_get_version_npm(npm_package):
45 | assert util.get_version() == "1.0.0"
46 | npm = util.normalize_path(shutil.which("npm"))
47 | run(f"{npm} version patch")
48 | assert util.get_version() == "1.0.1"
49 |
50 |
51 | def test_format_pr_entry(mock_github):
52 | gh = GhApi(owner="snuffy", repo="foo")
53 | info = gh.pulls.create("title", "head", "base", "body", True, False, None)
54 | resp = changelog.format_pr_entry("snuffy/foo", info["number"], auth="baz")
55 | assert resp.startswith("- ")
56 |
57 |
58 | def test_get_changelog_version_entry(py_package, mocker):
59 | version = util.get_version()
60 |
61 | mocked_gen = mocker.patch("jupyter_releaser.changelog.generate_activity_md")
62 | mocked_gen.return_value = testutil.CHANGELOG_ENTRY
63 | branch = "foo"
64 | util.run("git branch baz/bar")
65 | util.run("git tag v1.0 baz/bar")
66 | ref = "heads/baz/bar"
67 | resp = changelog.get_version_entry(ref, branch, "baz/bar", version)
68 | mocked_gen.assert_called_with(
69 | "baz/bar",
70 | since="v1.0",
71 | until=None,
72 | kind="pr",
73 | branch=branch,
74 | heading_level=2,
75 | auth=None,
76 | )
77 |
78 | assert f"## {version}" in resp
79 | assert testutil.PR_ENTRY in resp
80 |
81 | mocked_gen.return_value = testutil.CHANGELOG_ENTRY
82 | resp = changelog.get_version_entry(
83 | ref, branch, "baz/bar", version, resolve_backports=True, auth="bizz"
84 | )
85 | mocked_gen.assert_called_with(
86 | "baz/bar",
87 | since="v1.0",
88 | until=None,
89 | kind="pr",
90 | branch=branch,
91 | heading_level=2,
92 | auth="bizz",
93 | )
94 |
95 | assert f"## {version}" in resp
96 | assert testutil.PR_ENTRY in resp
97 |
98 |
99 | def test_get_changelog_version_entry_no_tag(py_package, mocker):
100 | version = util.get_version()
101 |
102 | mocked_gen = mocker.patch("jupyter_releaser.changelog.generate_activity_md")
103 | mocked_gen.return_value = testutil.CHANGELOG_ENTRY
104 | branch = "foo"
105 | util.run("git branch baz/bar")
106 | commit = run("git rev-list --max-parents=0 HEAD", quiet=True)
107 | ref = "heads/baz/bar"
108 | resp = changelog.get_version_entry(ref, branch, "baz/bar", version)
109 | mocked_gen.assert_called_with(
110 | "baz/bar",
111 | since=commit,
112 | until=None,
113 | kind="pr",
114 | branch=branch,
115 | heading_level=2,
116 | auth=None,
117 | )
118 |
119 | assert f"## {version}" in resp
120 | assert testutil.PR_ENTRY in resp
121 |
122 | mocked_gen.return_value = testutil.CHANGELOG_ENTRY
123 | resp = changelog.get_version_entry(
124 | ref, branch, "baz/bar", version, resolve_backports=True, auth="bizz"
125 | )
126 | mocked_gen.assert_called_with(
127 | "baz/bar",
128 | since=commit,
129 | until=None,
130 | kind="pr",
131 | branch=branch,
132 | heading_level=2,
133 | auth="bizz",
134 | )
135 |
136 | assert f"## {version}" in resp
137 | assert testutil.PR_ENTRY in resp
138 |
139 |
140 | def test_get_changelog_version_entry_since_last_stable(py_package, mocker):
141 | version = util.get_version()
142 |
143 | mocked_gen = mocker.patch("jupyter_releaser.changelog.generate_activity_md")
144 | mocked_gen.return_value = testutil.CHANGELOG_ENTRY
145 | branch = "foo"
146 | util.run("git branch baz/bar")
147 | util.run("git tag v1.0.0 baz/bar")
148 | util.run("git tag v1.1.0a0 baz/bar")
149 | ref = "heads/baz/bar"
150 | resp = changelog.get_version_entry(ref, branch, "baz/bar", version, since_last_stable=True)
151 | mocked_gen.assert_called_with(
152 | "baz/bar",
153 | since="v1.0.0",
154 | until=None,
155 | kind="pr",
156 | branch=branch,
157 | heading_level=2,
158 | auth=None,
159 | )
160 |
161 | assert f"## {version}" in resp
162 | assert testutil.PR_ENTRY in resp
163 |
164 |
165 | def test_get_empty_changelog(py_package, mocker):
166 | mocked_gen = mocker.patch("jupyter_releaser.changelog.generate_activity_md")
167 | mocked_gen.return_value = testutil.EMPTY_CHANGELOG_ENTRY
168 | branch = "foo"
169 | util.run("git branch baz/bar")
170 | ref = "heads/baz/bar"
171 | resp = changelog.get_version_entry(ref, branch, "baz/bar", "0.2.5", since="v0.2.4")
172 | mocked_gen.assert_called_with(
173 | "baz/bar",
174 | since="v0.2.4",
175 | until=None,
176 | kind="pr",
177 | branch=branch,
178 | heading_level=2,
179 | auth=None,
180 | )
181 |
182 | assert "...None" not in resp
183 |
184 |
185 | def test_splice_github_entry(py_package, mocker):
186 | version = util.get_version()
187 |
188 | mocked_gen = mocker.patch("jupyter_releaser.changelog.generate_activity_md")
189 | mocked_gen.return_value = testutil.CHANGELOG_ENTRY
190 | branch = "foo"
191 | util.run("git branch baz/bar")
192 | util.run("git tag v1.0.0 baz/bar")
193 | util.run("git tag v1.1.0a0 baz/bar")
194 | ref = "heads/baz/bar"
195 | resp = changelog.get_version_entry(ref, branch, "baz/bar", version, since_last_stable=True)
196 |
197 | updated = changelog.splice_github_entry(resp, testutil.GITHUB_CHANGELOG_ENTRY)
198 |
199 | assert "Defining contributions" in updated
200 |
201 | preamble = "# My title\nmy content\n"
202 | updated = changelog.splice_github_entry(resp, preamble + testutil.GITHUB_CHANGELOG_ENTRY)
203 |
204 | assert "Defining contributions" in updated
205 | assert preamble in updated
206 |
207 |
208 | def test_compute_sha256(py_package):
209 | assert len(util.compute_sha256(py_package / "CHANGELOG.md")) == 64
210 |
211 |
212 | def test_create_release_commit(py_package, build_mock):
213 | util.bump_version("0.0.2a0")
214 | version = util.get_version()
215 | util.run("pipx run build .")
216 | shas = util.create_release_commit(version)
217 | assert util.normalize_path("dist/foo-0.0.2a0.tar.gz") in shas
218 | assert util.normalize_path("dist/foo-0.0.2a0-py3-none-any.whl") in shas
219 |
220 |
221 | def test_create_release_commit_hybrid(py_package, build_mock):
222 | # Add an npm package and test with that
223 | util.bump_version("0.0.2a0")
224 | version = util.get_version()
225 | testutil.create_npm_package(py_package)
226 | pkg_json = py_package / "package.json"
227 | data = json.loads(pkg_json.read_text(encoding="utf-8"))
228 | data["version"] = version
229 | pkg_json.write_text(json.dumps(data, indent=4), encoding="utf-8")
230 | util.run("pre-commit run --all-files", check=False)
231 | txt = testutil.TBUMP_NPM_TEMPLATE
232 | (py_package / "tbump.toml").write_text(txt, encoding="utf-8")
233 |
234 | util.run("pipx run build .")
235 | shas = util.create_release_commit(version)
236 | assert len(shas) == 2
237 | assert util.normalize_path("dist/foo-0.0.2a0.tar.gz") in shas
238 |
239 |
240 | def test_handle_npm_config(npm_package):
241 | npmrc = Path("~/.npmrc").expanduser()
242 | existed = npmrc.exists()
243 | if existed:
244 | npmrc_text = npmrc.read_text(encoding="utf-8")
245 | npm.handle_npm_config("abc")
246 | text = npmrc.read_text(encoding="utf-8")
247 | assert "_authToken=abc" in text
248 |
249 | if existed:
250 | npmrc.write_text(npmrc_text, encoding="utf-8")
251 |
252 |
253 | def test_bump_version_reg(py_package):
254 | for spec in ["1.0.1", "1.0.3a4"]:
255 | util.bump_version(spec)
256 | util.run("git commit -a -m 'bump version'")
257 | assert util.get_version() == spec
258 | util.bump_version("1.0.2")
259 | util.bump_version("next")
260 | assert util.get_version() == "1.0.3"
261 | util.bump_version("patch")
262 | assert util.get_version() == "1.0.4"
263 | util.bump_version("1.0.3a5")
264 | util.bump_version("next")
265 | assert util.get_version() == "1.0.3a6"
266 | util.bump_version("minor")
267 | assert util.get_version() == "1.1.0"
268 |
269 |
270 | def test_bump_version_dev(py_package):
271 | util.bump_version("dev", changelog_path="CHANGELOG.md")
272 | assert util.get_version() == "0.1.0.dev0"
273 | util.bump_version("dev", changelog_path="CHANGELOG.md")
274 | assert util.get_version() == "0.1.0.dev1"
275 | util.bump_version("next", changelog_path="CHANGELOG.md")
276 | assert util.get_version() == "0.0.3"
277 | util.bump_version("dev", changelog_path="CHANGELOG.md")
278 | assert util.get_version() == "0.1.0.dev0"
279 | util.bump_version("patch", changelog_path="CHANGELOG.md")
280 | assert util.get_version() == "0.0.3"
281 | util.bump_version("dev", changelog_path="CHANGELOG.md")
282 | assert util.get_version() == "0.1.0.dev0"
283 | util.bump_version("minor", changelog_path="CHANGELOG.md")
284 | assert util.get_version() == "0.1.0"
285 |
286 |
287 | def test_get_config_python(py_package):
288 | Path(util.JUPYTER_RELEASER_CONFIG).unlink()
289 | text = util.PYPROJECT.read_text(encoding="utf-8")
290 | text = testutil.TOML_CONFIG.replace("\n[", "\n[tool.jupyter-releaser.")
291 | util.PYPROJECT.write_text(text, encoding="utf-8")
292 | config = util.read_config()
293 | assert "before-build-python" in config["hooks"]["before-build-python"]
294 |
295 |
296 | def test_get_config_npm(npm_package):
297 | Path(util.JUPYTER_RELEASER_CONFIG).unlink()
298 | package_json = util.PACKAGE_JSON
299 | data = json.loads(package_json.read_text(encoding="utf-8"))
300 | data["jupyter-releaser"] = toml.loads(testutil.TOML_CONFIG)
301 | package_json.write_text(json.dumps(data), encoding="utf-8")
302 | config = util.read_config()
303 | assert "before-build-npm" in config["hooks"]["before-build-npm"]
304 |
305 |
306 | def test_get_config_file(git_repo):
307 | config = util.read_config()
308 | assert "before-build-python" in config["hooks"]["before-build-python"]
309 |
310 |
311 | def test_get_latest_draft_release(mock_github):
312 | gh = GhApi(owner="foo", repo="bar")
313 | gh.create_release(
314 | "v1.0.0",
315 | "main",
316 | "v1.0.0",
317 | "body",
318 | True,
319 | True,
320 | files=[],
321 | )
322 | latest = util.latest_draft_release(gh)
323 | assert latest.name == "v1.0.0"
324 |
325 | # Ensure a different timestamp.
326 | time.sleep(1)
327 | gh.create_release(
328 | "v1.1.0",
329 | "bob",
330 | "v1.1.0",
331 | "body",
332 | True,
333 | True,
334 | files=[],
335 | )
336 | latest = util.latest_draft_release(gh)
337 | assert latest.name == "v1.1.0"
338 | latest = util.latest_draft_release(gh, "main")
339 | assert latest.name == "v1.0.0"
340 |
341 |
342 | def test_parse_release_url():
343 | match = util.parse_release_url("https://github.com/foo/bar/releases/tag/fizz")
344 | assert match.groupdict() == {"owner": "foo", "repo": "bar", "tag": "fizz"}
345 | match = util.parse_release_url("https://api.github.com/repos/fizz/buzz/releases/tags/foo")
346 | assert match.groupdict() == {"owner": "fizz", "repo": "buzz", "tag": "foo"}
347 | match = util.parse_release_url(
348 | "https://github.com/foo/bar/releases/tag/untagged-8a3c19f85a0a51d3ea66"
349 | )
350 | assert match.groupdict() == {
351 | "owner": "foo",
352 | "repo": "bar",
353 | "tag": "untagged-8a3c19f85a0a51d3ea66",
354 | }
355 |
356 |
357 | def test_extract_metadata_from_release_url(mock_github, draft_release):
358 | gh = GhApi(owner="foo", repo="bar")
359 | data = util.extract_metadata_from_release_url(gh, draft_release, "")
360 | assert os.environ["RH_BRANCH"] == data["branch"]
361 |
362 |
363 | def test_prepare_environment(mock_github, draft_release):
364 | os.environ["GITHUB_REPOSITORY"] = "foo/bar"
365 | tag = draft_release.split("/")[-1]
366 | os.environ["RH_BRANCH"] = "bar"
367 | os.environ["GITHUB_REF"] = f"refs/tag/{tag}"
368 | os.environ["RH_DRY_RUN"] = "true"
369 | data = util.prepare_environment()
370 | assert os.environ["RH_RELEASE_URL"] == draft_release
371 | assert data["version_spec"] == os.environ["RH_VERSION_SPEC"]
372 |
373 |
374 | def test_handle_since(npm_package, runner):
375 | runner(["prep-git", "--git-url", npm_package])
376 | since = util.handle_since()
377 | assert not since
378 |
379 | run("git tag v1.0.1", cwd=util.CHECKOUT_NAME)
380 | since = util.handle_since()
381 | assert since == "v1.0.1"
382 |
--------------------------------------------------------------------------------
/tests/test_mock_github.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import requests
4 | from ghapi.core import GhApi
5 |
6 | from jupyter_releaser.mock_github import Asset, Release, load_from_file, write_to_file
7 |
8 |
9 | def test_mock_github(mock_github):
10 | owner = "foo"
11 | repo_name = "bar"
12 | auth = "hi"
13 |
14 | gh = GhApi(owner=owner, repo=repo_name, token=auth)
15 | print(list(gh.repos.list_releases()))
16 |
17 | here = os.path.dirname(os.path.abspath(__file__))
18 | files = [os.path.join(here, f) for f in os.listdir(here)]
19 | files = [f for f in files if not os.path.isdir(f)]
20 |
21 | release = gh.create_release(
22 | "v1.0.0",
23 | "main",
24 | "v1.0.0",
25 | "body",
26 | True,
27 | True,
28 | files=files,
29 | )
30 |
31 | print(release.html_url)
32 |
33 | release = gh.repos.update_release(
34 | release["id"],
35 | release["tag_name"],
36 | release["target_commitish"],
37 | release["name"],
38 | "body",
39 | False,
40 | release["prerelease"],
41 | )
42 | assert release.draft is False
43 |
44 | for asset in release.assets:
45 | headers = dict(Authorization=f"token {auth}", Accept="application/octet-stream")
46 | print(asset.name)
47 | with requests.get(asset.url, headers=headers, stream=True, timeout=60) as r:
48 | r.raise_for_status()
49 | for _ in r.iter_content(chunk_size=8192):
50 | pass
51 |
52 | gh.git.create_ref("v1.1.0", "aaaa")
53 | tags = gh.list_tags("v1.1.0")
54 | assert tags[0]["object"]["sha"] == "aaaa"
55 |
56 | gh.repos.delete_release(release.id)
57 |
58 | pull = gh.pulls.create("title", "head", "base", "body", True, False, None)
59 | gh.issues.add_labels(pull.number, ["documentation"])
60 |
61 |
62 | def test_cache_storage():
63 | asset = Asset(
64 | id=1,
65 | name="hi",
66 | size=122,
67 | url="hi",
68 | content_type="hi",
69 | )
70 | model = Release(
71 | id=1,
72 | url="hi",
73 | html_url="ho",
74 | assets=[asset],
75 | upload_url="hi",
76 | created_at="1",
77 | draft=False,
78 | prerelease=False,
79 | target_commitish="1",
80 | tag_name="1",
81 | )
82 | write_to_file("releases", dict(test=model))
83 | data = load_from_file("releases", Release)
84 | assert isinstance(data["test"], Release)
85 | assert isinstance(data["test"].assets[0], Asset)
86 | assert data["test"].assets[0].url == asset.url
87 |
--------------------------------------------------------------------------------
/tests/util.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) Jupyter Development Team.
2 | # Distributed under the terms of the Modified BSD License.
3 | import json
4 | import os
5 | import shutil
6 | import tempfile
7 | from pathlib import Path
8 |
9 | from ghapi.core import GhApi
10 | from ruamel.yaml import YAML
11 |
12 | from jupyter_releaser import changelog, cli, util
13 | from jupyter_releaser.util import run
14 |
15 | VERSION_SPEC = "1.0.1"
16 |
17 | TOML_CONFIG = """
18 | [hooks]
19 | """
20 |
21 | for name in cli.main.commands:
22 | TOML_CONFIG += f"'before-{name}' = \"echo before-{name} >> 'log.txt'\"\n"
23 | TOML_CONFIG += f"'after-{name}' = \"echo after-{name} >> 'log.txt'\"\n"
24 |
25 | PR_ENTRY = "Mention the required GITHUB_ACCESS_TOKEN [#1](https://github.com/executablebooks/github-activity/pull/1) ([@consideRatio](https://github.com/consideRatio))"
26 |
27 | CHANGELOG_ENTRY = f"""
28 | ## master@{{2019-09-01}}...master@{{2019-11-01}}
29 |
30 | ([full changelog](https://github.com/executablebooks/github-activity/compare/479cc4b2f5504945021e3c4ee84818a10fabf810...ed7f1ed78b523c6b9fe6b3ac29e834087e299296))
31 |
32 | ### Merged PRs
33 |
34 | - defining contributions [#14](https://github.com/executablebooks/github-activity/pull/14) ([@choldgraf](https://github.com/choldgraf))
35 | - updating CLI for new tags [#12](https://github.com/executablebooks/github-activity/pull/12) ([@choldgraf](https://github.com/choldgraf))
36 | - fixing link to changelog with refs [#11](https://github.com/executablebooks/github-activity/pull/11) ([@choldgraf](https://github.com/choldgraf))
37 | - adding contributors list [#10](https://github.com/executablebooks/github-activity/pull/10) ([@choldgraf](https://github.com/choldgraf))
38 | - some improvements to `since` and opened issues list [#8](https://github.com/executablebooks/github-activity/pull/8) ([@choldgraf](https://github.com/choldgraf))
39 | - Support git references etc. [#6](https://github.com/executablebooks/github-activity/pull/6) ([@consideRatio](https://github.com/consideRatio))
40 | - adding authentication information [#2](https://github.com/executablebooks/github-activity/pull/2) ([@choldgraf](https://github.com/choldgraf))
41 | - {PR_ENTRY}
42 |
43 | ### Contributors to this release
44 |
45 | ([GitHub contributors page for this release](https://github.com/executablebooks/github-activity/graphs/contributors?from=2019-09-01&to=2019-11-01&type=c))
46 |
47 | [@betatim](https://github.com/search?q=repo%3Aexecutablebooks%2Fgithub-activity+involves%3Abetatim+updated%3A2019-09-01..2019-11-01&type=Issues) | [@choldgraf](https://github.com/search?q=repo%3Aexecutablebooks%2Fgithub-activity+involves%3Acholdgraf+updated%3A2019-09-01..2019-11-01&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Aexecutablebooks%2Fgithub-activity+involves%3AconsideRatio+updated%3A2019-09-01..2019-11-01&type=Issues)
48 | """
49 |
50 | EMPTY_CHANGELOG_ENTRY = """
51 | ## main@{2021-09-15}...main@{2022-01-18}
52 |
53 | ([Full Changelog](https://github.com/QuantStack/jupyterlab-js-logs/compare/v0.2.4...None))
54 |
55 | ### Contributors to this release
56 |
57 | ([GitHub contributors page for this release](https://github.com/QuantStack/jupyterlab-js-logs/graphs/contributors?from=2021-09-15&to=2022-01-18&type=c))
58 |
59 | """
60 |
61 | GITHUB_CHANGELOG_ENTRY = """
62 | ## What's Changed
63 | * Some improvements to `since` and opened issues list @choldgraf in https://github.com/executablebooks/github-activity/pull/8
64 | * Defining contributions by @choldgraf in https://github.com/executablebooks/github-activity/pull/14
65 | * Fixing link to changelog with refs by @choldgraf in https://github.com/executablebooks/github-activity/pull/11
66 |
67 |
68 | **Full Changelog**: https://github.com/executablebooks/github-activity/compare/479cc4b2f5504945021e3c4ee84818a10fabf810...ed7f1ed78b523c6b9fe6b3ac29e834087e299296
69 | """
70 |
71 |
72 | BASE_RELEASE_METADATA = dict(
73 | version_spec="foo",
74 | branch="bar",
75 | repo="fizz",
76 | since="buzz",
77 | since_last_stable=False,
78 | post_version_spec="dev",
79 | post_version_message="hi",
80 | silent=False,
81 | )
82 |
83 |
84 | def setup_cfg_template(package_name="foo", module_name=None):
85 | return f"""
86 | [metadata]
87 | name = {package_name}
88 | version = attr: {module_name or package_name}.__version__
89 |
90 | [options]
91 | zip_safe = False
92 | include_package_data = True
93 | py_modules = {module_name or package_name}
94 | """
95 |
96 |
97 | SETUP_PY_TEMPLATE = """__import__("setuptools").setup()\n"""
98 |
99 | LICENSE_TEMPLATE = "A fake license\n"
100 |
101 | README_TEMPLATE = "A fake readme\n"
102 |
103 |
104 | def pyproject_template(project_name="foo", module_name="foo", sub_packages=None):
105 | sub_packages = sub_packages or []
106 | res = f"""
107 | [build-system]
108 | requires = ["hatchling>=1.11"]
109 | build-backend = "hatchling.build"
110 |
111 | [project]
112 | name = "{project_name}"
113 | dynamic = ["version"]
114 | description = "My package description"
115 | readme = "README.md"
116 | license = {{file = "LICENSE"}}
117 | authors = [
118 | {{email = "foo@foo.com"}},
119 | {{name = "foo"}}
120 | ]
121 |
122 | [tool.hatch.version]
123 | path = "{module_name}.py"
124 | validate-bump = false
125 |
126 | [project.urls]
127 | homepage = "https://foo.com"
128 | """
129 | if sub_packages:
130 | res += f"""
131 | [tools.jupyter-releaser.options]
132 | python_packages = {sub_packages}
133 | """
134 | return res
135 |
136 |
137 | PY_MODULE_TEMPLATE = '__version__ = "0.0.1"\n'
138 |
139 | TBUMP_NPM_TEMPLATE = """
140 | [[file]]
141 | src = "package.json"
142 | search = '"version": "{current_version}"'
143 | """
144 |
145 | MANIFEST_TEMPLATE = """
146 | include *.md
147 | include *.toml
148 | include *.yaml
149 | include LICENSE
150 | """
151 |
152 | CHANGELOG_TEMPLATE = f"""# Changelog
153 |
154 | {changelog.START_MARKER}
155 |
156 | ## 0.0.2
157 |
158 | Second commit
159 |
160 | {changelog.END_MARKER}
161 |
162 | ## 0.0.1
163 |
164 | Initial commit
165 | """
166 |
167 |
168 | def mock_changelog_entry(package_path, runner, mocker, version_spec=VERSION_SPEC):
169 | runner(["bump-version", "--version-spec", version_spec])
170 | changelog_file = "CHANGELOG.md"
171 | changelog = Path(util.CHECKOUT_NAME) / changelog_file
172 | mocked_gen = mocker.patch("jupyter_releaser.changelog.generate_activity_md")
173 | mocked_gen.return_value = CHANGELOG_ENTRY
174 | runner(["build-changelog", "--changelog-path", changelog_file])
175 | return changelog_file
176 |
177 |
178 | def create_npm_package(git_repo):
179 | npm = util.normalize_path(shutil.which("npm"))
180 | run(f"{npm} init -y")
181 |
182 | # Add the npm provenance info.
183 | pack_json = Path(git_repo / "package.json")
184 | with pack_json.open() as fid:
185 | data = json.load(fid)
186 | data["repository"] = dict(url=str(git_repo))
187 | with pack_json.open("w") as fid:
188 | json.dump(data, fid)
189 |
190 | git_repo.joinpath("index.js").write_text('console.log("hello");\n', encoding="utf-8")
191 |
192 | run("git add .")
193 | run('git commit -m "initial npm package"')
194 |
195 | run("git checkout foo")
196 | run("git pull origin bar", quiet=True)
197 | run("git checkout bar")
198 | return git_repo
199 |
200 |
201 | def get_log():
202 | log = Path(util.CHECKOUT_NAME) / "log.txt"
203 | return log.read_text(encoding="utf-8").splitlines()
204 |
205 |
206 | def create_python_package(git_repo, multi=False, not_matching_name=False):
207 | def write_files(git_repo, sub_packages=None, package_name="foo", module_name=None):
208 | sub_packages = sub_packages or []
209 |
210 | module_name = module_name or package_name
211 |
212 | pyproject = git_repo / "pyproject.toml"
213 | pyproject.write_text(
214 | pyproject_template(package_name, module_name, sub_packages), encoding="utf-8"
215 | )
216 |
217 | foopy = git_repo / f"{module_name}.py"
218 | foopy.write_text(PY_MODULE_TEMPLATE, encoding="utf-8")
219 |
220 | license = git_repo / "LICENSE"
221 | license.write_text(LICENSE_TEMPLATE, encoding="utf-8")
222 |
223 | here = Path(__file__).parent
224 | text = here.parent.joinpath(".pre-commit-config.yaml").read_text(encoding="utf-8")
225 |
226 | # Remove sp-repo-review and don't check yaml files.
227 | yaml = YAML(typ="safe")
228 | table = yaml.load(text)
229 | for item in list(table["repos"]):
230 | if item["repo"] == "https://github.com/scientific-python/cookie":
231 | table["repos"].remove(item)
232 |
233 | pre_commit = git_repo / ".pre-commit-config.yaml"
234 | with open(str(pre_commit), "w") as fid:
235 | yaml.dump(table, fid)
236 |
237 | readme = git_repo / "README.md"
238 | readme.write_text(README_TEMPLATE, encoding="utf-8")
239 |
240 | sub_packages = []
241 | if multi:
242 | packages = [{"abs_path": git_repo, "rel_path": "."}]
243 | for i in range(2):
244 | sub_package = Path(f"sub_package{i}")
245 | sub_packages.append(str(sub_package))
246 | packages.append(
247 | {
248 | "abs_path": git_repo / sub_package,
249 | "rel_path": sub_package,
250 | }
251 | )
252 | sub_package.mkdir()
253 | package_name = f"foo{i}"
254 | module_name = f"foo{i}bar" if not_matching_name else None
255 | write_files(
256 | git_repo / sub_package,
257 | package_name=package_name,
258 | module_name=module_name,
259 | )
260 | run(f"git add {sub_package}")
261 | run(f'git commit -m "initial python {sub_package}"')
262 |
263 | package_name = "foo"
264 | module_name = "foobar" if not_matching_name else None
265 | write_files(
266 | git_repo,
267 | sub_packages=sub_packages,
268 | package_name=package_name,
269 | module_name=module_name,
270 | )
271 | run("git add .")
272 | run('git commit -m "initial python package"')
273 |
274 | run("git checkout foo")
275 | run("git pull origin bar", quiet=True)
276 | run("git checkout bar")
277 |
278 | if multi:
279 | return packages
280 | return git_repo
281 |
282 |
283 | def create_draft_release(ref="bar", files=None):
284 | gh = GhApi("foo", "bar")
285 | release = gh.create_release(ref, "bar", ref, "body", True, True)
286 | if files:
287 | with tempfile.TemporaryDirectory() as td:
288 | metadata_file = os.path.join(td, "metadata.json")
289 | with open(metadata_file, "w") as fid:
290 | fid.write("{}")
291 | gh.upload_file(release, metadata_file)
292 | release = util.release_for_url(gh, release.url)
293 | util.upload_assets(gh, files, release, "foo")
294 | return release
295 |
--------------------------------------------------------------------------------