diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000000..ccbf0ff248
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+/.github @DisnakeDev/maintainers
+/scripts/ci @DisnakeDev/maintainers
diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml
new file mode 100644
index 0000000000..692affbd8b
--- /dev/null
+++ b/.github/workflows/build-release.yaml
@@ -0,0 +1,227 @@
+# SPDX-License-Identifier: MIT
+
+name: Build (+ Release)
+
+# test build for commit/tag, but only upload release for tags
+on:
+ push:
+ branches:
+ - "master"
+ - 'v[0-9]+.[0-9]+.x' # matches to backport branches, e.g. v3.6.x
+ tags:
+ - "v[0-9]+.[0-9]+.[0-9]+"
+
+permissions:
+ contents: read
+
+jobs:
+ # Builds sdist and wheel, runs `twine check`, and optionally uploads artifacts.
+ build:
+ name: Build package
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up environment
+ id: setup
+ uses: ./.github/actions/setup-env
+ with:
+ python-version: 3.8
+
+ - name: Install dependencies
+ run: pdm install -dG build
+
+ - name: Build package
+ run: |
+ pdm run python -m build
+ ls -la dist/
+
+ - name: Twine check
+ run: pdm run twine check --strict dist/*
+
+ - name: Show metadata
+ run: |
+ mkdir out/
+ tar -xf dist/*.tar.gz -C out/
+
+ echo -e "Metadata
\n" >> $GITHUB_STEP_SUMMARY
+ cat out/*/PKG-INFO | sed 's/^/ /' | tee -a $GITHUB_STEP_SUMMARY
+ echo -e "\n \n" >> $GITHUB_STEP_SUMMARY
+
+ - name: Upload artifact
+ # only upload artifacts when necessary
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: dist/
+ if-no-files-found: error
+
+
+ ### Anything below this only runs for tags ###
+
+ # Ensures that git tag and built version match.
+ validate-tag:
+ name: Validate tag
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ needs:
+ - build
+ env:
+ GIT_TAG: ${{ github.ref_name }}
+ outputs:
+ bump_dev: ${{ steps.check-dev.outputs.bump_dev }}
+
+ steps:
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist/
+
+ - name: Compare sdist version to git tag
+ run: |
+ mkdir out/
+ tar -xf dist/*.tar.gz -C out/
+
+ SDIST_VERSION="$(grep "^Version:" out/*/PKG-INFO | cut -d' ' -f2-)"
+ echo "git tag: $GIT_TAG"
+ echo "sdist version: $SDIST_VERSION"
+
+ if [ "$GIT_TAG" != "v$SDIST_VERSION" ]; then
+ echo "error: git tag does not match sdist version" >&2
+ exit 1
+ fi
+
+ - name: Determine if dev version PR is needed
+ id: check-dev
+ run: |
+ BUMP_DEV=
+ # if this is a new major/minor version, create a PR later
+ if [[ "$GIT_TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
+ BUMP_DEV=1
+ fi
+ echo "bump_dev=$BUMP_DEV" | tee -a $GITHUB_OUTPUT
+
+
+ # Creates a draft release on GitHub, and uploads the artifacts there.
+ release-github:
+ name: Create GitHub draft release
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ needs:
+ - build
+ - validate-tag
+ permissions:
+ contents: write # required for creating releases
+
+ steps:
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist/
+
+ - name: Calculate versions
+ id: versions
+ env:
+ GIT_TAG: ${{ github.ref_name }}
+ run: |
+ # v1.2.3 -> v1-2-3 (for changelog)
+ echo "docs_version=${GIT_TAG//./-}" >> $GITHUB_OUTPUT
+
+ - name: Create Release
+ uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
+ with:
+ files: dist/*
+ draft: true
+ body: |
+ TBD.
+
+ **Changelog**: https://docs.disnake.dev/en/stable/whats_new.html#${{ steps.versions.outputs.docs_version }}
+ **Git history**: https://github.com/${{ github.repository }}/compare/vTODO...${{ github.ref_name }}
+
+
+ # Creates a PyPI release (using an environment which requires separate confirmation).
+ release-pypi:
+ name: Publish package to pypi.org
+ environment:
+ name: release-pypi
+ url: https://pypi.org/project/disnake/
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ needs:
+ - build
+ - validate-tag
+ permissions:
+ id-token: write # this permission is mandatory for trusted publishing
+
+ steps:
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist/
+
+ - name: Upload to pypi
+ uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # v1.8.7
+ with:
+ print-hash: true
+
+
+ # Creates a PR to bump to an alpha version for development, if applicable.
+ create-dev-version-pr:
+ name: Create dev version bump PR
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/') && needs.validate-tag.outputs.bump_dev
+ needs:
+ - validate-tag
+ - release-github
+ - release-pypi
+
+ steps:
+ # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow
+ - name: Generate app token
+ id: generate_token
+ uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
+ with:
+ app_id: ${{ secrets.BOT_APP_ID }}
+ private_key: ${{ secrets.BOT_PRIVATE_KEY }}
+
+ - uses: actions/checkout@v3
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ persist-credentials: false
+ ref: master # the PR action wants a proper base branch
+
+ - name: Set git name/email
+ env:
+ GIT_USER: ${{ vars.GIT_APP_USER_NAME }}
+ GIT_EMAIL: ${{ vars.GIT_APP_USER_EMAIL }}
+ run: |
+ git config user.name "$GIT_USER"
+ git config user.email "$GIT_EMAIL"
+
+ - name: Update version to dev
+ id: update-version
+ run: |
+ NEW_VERSION="$(python scripts/ci/versiontool.py --set dev)"
+ git commit -a -m "chore: update version to v$NEW_VERSION"
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
+
+ - name: Create pull request
+ uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ branch: auto/dev-v${{ steps.update-version.outputs.new_version }}
+ delete-branch: true
+ base: master
+ title: "chore: update version to v${{ steps.update-version.outputs.new_version }}"
+ body: |
+ Automated dev version PR.
+
+ https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ labels: |
+ skip news
+ t: meta
diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml
index 8fdb8e27c7..f2a989c0c4 100644
--- a/.github/workflows/changelog.yaml
+++ b/.github/workflows/changelog.yaml
@@ -33,7 +33,7 @@ jobs:
python-version: '3.9'
- name: Install dependencies
- run: pdm install -dG tools
+ run: pdm install -dG changelog
- name: Check for presence of a Change Log fragment (only pull requests)
# NOTE: The pull request' base branch needs to be fetched so towncrier
diff --git a/.github/workflows/create-release-pr.yaml b/.github/workflows/create-release-pr.yaml
new file mode 100644
index 0000000000..56c32a4ac5
--- /dev/null
+++ b/.github/workflows/create-release-pr.yaml
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: MIT
+
+name: Create Release PR
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "The new version number, e.g. `1.2.3`."
+ type: string
+ required: true
+
+permissions: {}
+
+jobs:
+ create-release-pr:
+ name: Create Release PR
+ runs-on: ubuntu-latest
+
+ env:
+ VERSION_INPUT: ${{ inputs.version }}
+
+ steps:
+ # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow
+ - name: Generate app token
+ id: generate_token
+ uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
+ with:
+ app_id: ${{ secrets.BOT_APP_ID }}
+ private_key: ${{ secrets.BOT_PRIVATE_KEY }}
+
+ - uses: actions/checkout@v3
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ persist-credentials: false
+
+ - name: Set git name/email
+ env:
+ GIT_USER: ${{ vars.GIT_APP_USER_NAME }}
+ GIT_EMAIL: ${{ vars.GIT_APP_USER_EMAIL }}
+ run: |
+ git config user.name "$GIT_USER"
+ git config user.email "$GIT_EMAIL"
+
+ - name: Set up environment
+ uses: ./.github/actions/setup-env
+ with:
+ python-version: 3.8
+
+ - name: Install dependencies
+ run: pdm install -dG changelog
+
+ - name: Update version
+ run: |
+ python scripts/ci/versiontool.py --set "$VERSION_INPUT"
+ git commit -a -m "chore: update version to $VERSION_INPUT"
+
+ - name: Build changelog
+ run: |
+ pdm run towncrier build --yes --version "$VERSION_INPUT"
+ git commit -a -m "docs: build changelog"
+
+ - name: Create pull request
+ uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ branch: auto/release-v${{ inputs.version }}
+ delete-branch: true
+ title: "release: v${{ inputs.version }}"
+ body: |
+ Automated release PR, triggered by @${{ github.actor }} for ${{ github.sha }}.
+
+ ### Tasks
+ - [ ] Add changelogs from backports, if applicable.
+ - [ ] Once merged, create + push a tag.
+
+ https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ labels: |
+ t: release
+ assignees: |
+ ${{ github.actor }}
diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index 696bdc5fb0..64dc2e18a7 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -6,7 +6,7 @@ on:
push:
branches:
- 'master'
- - 'v[0-9]+.[0-9]+.x' # matches to backport branches, e.g. 3.6
+ - 'v[0-9]+.[0-9]+.x' # matches to backport branches, e.g. v3.6.x
- 'run-ci/*'
tags:
pull_request:
@@ -124,10 +124,11 @@ jobs:
run: nox -s check-manifest
# This only runs if the previous steps were successful, no point in running it otherwise
- - name: Build package
+ - name: Try building package
run: |
- python -m pip install -U build
- python -m build
+ pdm install -dG build
+ pdm run python -m build
+ ls -la dist/
# run the libcst parsers and check for changes
- name: libcst codemod
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0c599e6c6d..104e2264a4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,7 +35,7 @@ repos:
args: [--negate]
types: [text]
exclude_types: [json, pofile]
- exclude: 'changelog/|py.typed|disnake/bin/COPYING|.github/PULL_REQUEST_TEMPLATE.md|LICENSE|MANIFEST.in'
+ exclude: 'changelog/|py.typed|disnake/bin/COPYING|.github/PULL_REQUEST_TEMPLATE.md|.github/CODEOWNERS|LICENSE|MANIFEST.in'
- repo: https://github.com/pycqa/isort
rev: 5.12.0
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 0000000000..3919b696e5
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,47 @@
+
+
+# Release Procedure
+
+This document provides general information and steps about the project's release procedure.
+If you're reading this, this will likely not be useful to you, unless you have administrator permissions in the repository or want to replicate this setup in your own project :p
+
+The process is largely automated, with manual action only being needed where higher permissions are required.
+Note that pre-releases (alpha/beta/rc) don't quite work with the current setup; we don't currently anticipate making pre-releases, but this may still be improved in the future.
+
+
+## Steps
+
+These steps are mostly equivalent for major/minor (feature) and micro (bugfix) releases.
+The branch should be `master` for major/minor releases and e.g. `1.2.x` for micro releases.
+
+1. Run the `Create Release PR` workflow from the GitHub UI (or CLI), specifying the correct branch and new version.
+ 1. Wait until a PR containing the changelog and version bump is created. Update the changelog description and merge the PR.
+ 2. In the CLI, fetch changes and create + push a tag for the newly created commit, which will trigger another workflow.
+ - [if latest] Also force-push a `stable` tag for the same ref.
+ 3. Update the visibility of old/new versions on https://readthedocs.org.
+2. Approve the environment deployment when prompted, which will push the package to PyPI.
+ 1. Update and publish the created GitHub draft release, as well as a Discord announcement. 🎉
+3. [if major/minor] Create a `v1.2.x` branch for future backports, and merge the newly created dev version PR.
+
+
+### Manual Steps
+
+If the automated process above does not work for some reason, here's the abridged version of the manual release process:
+
+1. Update version in `__init__.py`, run `towncrier build`. Commit, push, create + merge PR.
+2. Follow steps 1.ii. + 1.iii. like above.
+3. Run `python -m build`, attach artifacts to GitHub release.
+4. Run `twine check dist/*` + `twine upload dist/*`.
+5. Follow steps 2.i. + 3. like above.
+
+
+## Repository Setup
+
+This automated process requires some initial one-time setup in the repository to work properly:
+
+1. Create a GitHub App ([docs](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow)), enable write permissions for `content` and `pull_requests`.
+2. Install the app in the repository.
+3. Set repository variables `GIT_APP_USER_NAME` and `GIT_APP_USER_EMAIL` accordingly.
+4. Set repository secrets `BOT_APP_ID` and `BOT_PRIVATE_KEY`.
+5. Create a `release-pypi` environment, add protection rules.
+6. Set up trusted publishing on PyPI ([docs](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)).
diff --git a/disnake/__init__.py b/disnake/__init__.py
index 396bab5e43..0cfd1b53d0 100644
--- a/disnake/__init__.py
+++ b/disnake/__init__.py
@@ -81,6 +81,8 @@ class VersionInfo(NamedTuple):
serial: int
+# fmt: off
version_info: VersionInfo = VersionInfo(major=2, minor=10, micro=0, releaselevel="alpha", serial=0)
+# fmt: on
logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/pyproject.toml b/pyproject.toml
index 984bdf767f..4756d55c4a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,10 +70,12 @@ tools = [
"pre-commit~=3.0",
"slotscheck~=0.16.4",
"python-dotenv~=1.0.0",
- "towncrier==23.6.0",
"check-manifest==0.49",
"ruff==0.0.292",
]
+changelog = [
+ "towncrier==23.6.0",
+]
codemod = [
# run codemods on the respository (mostly automated typing)
"libcst~=1.1.0",
@@ -94,6 +96,11 @@ test = [
"looptime~=0.2",
"coverage[toml]~=6.5.0",
]
+build = [
+ "wheel~=0.40.0",
+ "build~=0.10.0",
+ "twine~=4.0.2",
+]
[tool.pdm.scripts]
black = { composite = ["lint black"], help = "Run black" }
@@ -230,8 +237,8 @@ ignore = [
"T201", # print found, printing is currently accepted in the test bot
"PT", # this is not a module of pytest tests
]
-"scripts/*.py" = ["S101"] # use of assert is okay in scripts
"tests/*.py" = ["S101"] # use of assert is okay in test files
+"scripts/*.py" = ["S101"] # use of assert is okay in scripts
# we are not using noqa in the example files themselves
"examples/*.py" = [
"B008", # do not perform function calls in argument defaults, this is how most commands work
@@ -378,6 +385,7 @@ ignore = [
"noxfile.py",
# docs
"CONTRIBUTING.md",
+ "RELEASE.md",
"assets/**",
"changelog/**",
"docs/**",
diff --git a/scripts/ci/versiontool.py b/scripts/ci/versiontool.py
new file mode 100644
index 0000000000..cec50cff3d
--- /dev/null
+++ b/scripts/ci/versiontool.py
@@ -0,0 +1,128 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from enum import Enum
+from pathlib import Path
+from typing import NamedTuple, NoReturn
+
+TARGET_FILE = Path("disnake/__init__.py")
+ORIG_INIT_CONTENTS = TARGET_FILE.read_text("utf-8")
+
+version_re = re.compile(r"(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)(\d+)?)?")
+
+
+class ReleaseLevel(Enum):
+ alpha = "a"
+ beta = "b"
+ candidate = "rc"
+ final = ""
+
+
+class VersionInfo(NamedTuple):
+ major: int
+ minor: int
+ micro: int
+ releaselevel: ReleaseLevel
+ serial: int
+
+ @classmethod
+ def from_str(cls, s: str) -> VersionInfo:
+ match = version_re.fullmatch(s)
+ if not match:
+ raise ValueError(f"invalid version: '{s}'")
+
+ major, minor, micro, releaselevel, serial = match.groups()
+ return VersionInfo(
+ int(major),
+ int(minor),
+ int(micro),
+ ReleaseLevel(releaselevel or ""),
+ int(serial or 0),
+ )
+
+ def __str__(self) -> str:
+ s = f"{self.major}.{self.minor}.{self.micro}"
+ if self.releaselevel is not ReleaseLevel.final:
+ s += self.releaselevel.value
+ if self.serial:
+ s += str(self.serial)
+ return s
+
+ def to_versioninfo(self) -> str:
+ return (
+ f"VersionInfo(major={self.major}, minor={self.minor}, micro={self.micro}, "
+ f'releaselevel="{self.releaselevel.name}", serial={self.serial})'
+ )
+
+
+def get_current_version() -> VersionInfo:
+ match = re.search(r"^__version__\b.*\"(.+?)\"$", ORIG_INIT_CONTENTS, re.MULTILINE)
+ assert match, "could not find current version in __init__.py"
+ return VersionInfo.from_str(match[1])
+
+
+def replace_line(text: str, regex: str, repl: str) -> str:
+ lines = []
+ found = False
+
+ for line in text.split("\n"):
+ if re.search(regex, line):
+ found = True
+ line = repl
+ lines.append(line)
+
+ assert found, f"failed to find `{regex}` in file"
+ return "\n".join(lines)
+
+
+def fail(msg: str) -> NoReturn:
+ print("error:", msg, file=sys.stderr)
+ sys.exit(1)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument("--set", metavar="VERSION", help="set new version (e.g. '1.2.3' or 'dev')")
+ group.add_argument("--show", action="store_true", help="print current version")
+ args = parser.parse_args()
+
+ current_version = get_current_version()
+
+ if args.show:
+ print(str(current_version))
+ return
+
+ # else, update to specified version
+ new_version_str = args.set
+
+ if new_version_str == "dev":
+ if current_version.releaselevel is not ReleaseLevel.final:
+ fail("Current version must be final to bump to dev version")
+ new_version = VersionInfo(
+ major=current_version.major,
+ minor=current_version.minor + 1,
+ micro=0,
+ releaselevel=ReleaseLevel.alpha,
+ serial=0,
+ )
+ else:
+ new_version = VersionInfo.from_str(new_version_str)
+
+ text = ORIG_INIT_CONTENTS
+ text = replace_line(text, r"^__version__\b", f'__version__ = "{new_version!s}"')
+ text = replace_line(
+ text, r"^version_info\b", f"version_info: VersionInfo = {new_version.to_versioninfo()}"
+ )
+
+ if text != ORIG_INIT_CONTENTS:
+ TARGET_FILE.write_text(text, "utf-8")
+
+ print(str(new_version))
+
+
+main()