diff --git a/.github/scripts/bump_version.py b/.github/scripts/bump_version.py new file mode 100755 index 000000000..63f52a524 --- /dev/null +++ b/.github/scripts/bump_version.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Compute and commit the next release version. + +Reads the current version from pyproject.toml, computes the next version per +the requested mode, writes it back, refreshes uv.lock, and commits. The +computed version is printed to stdout for callers to capture. + +Modes: + rc — X.Y.ZrcN -> X.Y.Zrc(N+1) + final — X.Y.0rcN -> X.Y.0 (first final of the minor) + patch-rc — X.Y.Z -> X.Y.(Z+1)rc0 | X.Y.(Z+1)rcN -> X.Y.(Z+1)rc(N+1) + patch-final — X.Y.ZrcN (Z>0) -> X.Y.Z (promote patch rc to final) + dev — X.Y.Z.devN -> X.Y.Z.dev(N+1) (main-only) + +`dev` mode runs on `main` and iterates its .devN counter. All other modes +run on `release/v*` branches. + +With --dry-run the script prints the proposed version and exits without +writing or committing. +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import tomllib +from pathlib import Path + +from packaging.version import Version + +REPO_ROOT = Path(__file__).resolve().parents[2] +PYPROJECT = REPO_ROOT / "pyproject.toml" + + +def read_current_version() -> Version: + with PYPROJECT.open("rb") as f: + data = tomllib.load(f) + raw = data["project"]["version"] + return Version(raw) + + +def existing_tags() -> set[str]: + out = subprocess.run( + ["git", "tag", "--list", "v*"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ) + return {line.strip() for line in out.stdout.splitlines() if line.strip()} + + +def current_branch() -> str: + out = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ) + return out.stdout.strip() + + +def compute_next(current: Version, mode: str) -> Version: + """Compute the next version per mode. Raises ValueError on disallowed transitions.""" + major, minor, patch = ( + current.release[0], + current.release[1], + (current.release[2] if len(current.release) > 2 else 0), + ) + + if mode == "dev": + if current.dev is None: + raise ValueError( + f"mode=dev requires current version to be a .dev release; got {current}." + ) + if current.pre is not None: + raise ValueError( + f"mode=dev does not support .devN combined with a pre-release " + f"segment; got {current}." + ) + return Version(f"{major}.{minor}.{patch}.dev{current.dev + 1}") + + if current.dev is not None: + raise ValueError( + f"Current version {current} is a .dev release; mode {mode!r} only " + "operates on release branches (rc/final). Ran on the wrong branch?" + ) + + if mode == "rc": + if current.pre is None or current.pre[0] != "rc": + raise ValueError( + f"mode=rc requires current version to be an rc; got {current}. " + "If this is a final, use mode=patch-rc to start a patch cycle." + ) + return Version(f"{major}.{minor}.{patch}rc{current.pre[1] + 1}") + + if mode == "final": + if current.pre is None or current.pre[0] != "rc": + raise ValueError( + f"mode=final requires current version to be an rc; got {current}." + ) + if patch != 0: + raise ValueError( + f"mode=final is for promoting minor rcs (X.Y.0rcN -> X.Y.0); " + f"got patch version {current}. Use mode=patch-final for patches." + ) + return Version(f"{major}.{minor}.{patch}") + + if mode == "patch-rc": + if current.pre is None: + return Version(f"{major}.{minor}.{patch + 1}rc0") + if current.pre[0] != "rc": + raise ValueError(f"Unexpected pre-release segment in {current}") + if patch == 0: + raise ValueError( + f"mode=patch-rc requires an existing final or patch-rc; got " + f"{current} which is a minor rc. Use mode=rc to iterate minor rcs." + ) + return Version(f"{major}.{minor}.{patch}rc{current.pre[1] + 1}") + + if mode == "patch-final": + if current.pre is None or current.pre[0] != "rc": + raise ValueError( + f"mode=patch-final requires current to be a patch rc; got {current}." + ) + if patch == 0: + raise ValueError( + f"mode=patch-final is for patches (Z>0); got {current}. " + "Use mode=final to promote a minor rc." + ) + return Version(f"{major}.{minor}.{patch}") + + raise ValueError(f"Unknown mode: {mode!r}") + + +def write_pyproject(new_version: Version) -> None: + content = PYPROJECT.read_text() + pattern = re.compile(r'^(version\s*=\s*")[^"]+(")', re.MULTILINE) + new_content, n = pattern.subn(rf"\g<1>{new_version}\g<2>", content, count=1) + if n != 1: + raise RuntimeError("Failed to locate version line in pyproject.toml") + PYPROJECT.write_text(new_content) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--mode", + required=True, + choices=["rc", "final", "patch-rc", "patch-final", "dev"], + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print the proposed next version and exit without writing or committing.", + ) + parser.add_argument( + "--skip-branch-check", + action="store_true", + help="Skip the branch assertion. For local testing only.", + ) + args = parser.parse_args() + + if not args.skip_branch_check: + branch = current_branch() + if args.mode == "dev": + if branch != "main": + print( + f"error: mode=dev must run on main; current branch is {branch!r}", + file=sys.stderr, + ) + return 2 + elif not branch.startswith("release/v"): + print( + f"error: mode={args.mode} must run on a release/v* branch; " + f"current is {branch!r}", + file=sys.stderr, + ) + return 2 + + current = read_current_version() + try: + next_version = compute_next(current, args.mode) + except ValueError as e: + print(f"error: {e}", file=sys.stderr) + return 2 + + tag = f"v{next_version}" + if tag in existing_tags(): + print( + f"error: tag {tag} already exists; refusing to overwrite", file=sys.stderr + ) + return 2 + + if args.dry_run: + print(next_version) + return 0 + + write_pyproject(next_version) + # Override UV_FROZEN inherited from the workflow env: frozen mode rejects + # lockfile updates, but every bump changes the package entry. + subprocess.run( + ["uv", "lock", "--upgrade-package", "mellea"], + cwd=REPO_ROOT, + check=True, + env={**os.environ, "UV_FROZEN": "0"}, + ) + subprocess.run( + ["git", "add", "pyproject.toml", "uv.lock"], cwd=REPO_ROOT, check=True + ) + subprocess.run( + ["git", "commit", "-m", f"release: bump version to {next_version} [skip ci]"], + cwd=REPO_ROOT, + check=True, + ) + + print(next_version) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/cut_release_branch.sh b/.github/scripts/cut_release_branch.sh new file mode 100755 index 000000000..38806d3f4 --- /dev/null +++ b/.github/scripts/cut_release_branch.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# Cut a release branch off main and apply matched version bumps to both branches. +# +# Expected state before running: +# - Checked out on main with a clean working tree +# - pyproject.toml version matches X.Y.0.devN +# - No existing tag v{X.Y.0rc0} or branch release/vX.Y +# +# Produces: +# - release/vX.Y branch at X.Y.0rc0, pushed to origin +# - main bumped to X.(Y+1).0.dev0, pushed to origin +# +# Env: +# CONFIRM_MINOR (optional) — if set, must match X.Y derived from pyproject. + +set -eu +set -x + +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [ "${CURRENT_BRANCH}" != "main" ]; then + >&2 echo "error: must be run from main, got ${CURRENT_BRANCH}" + exit 2 +fi + +if [ -n "$(git status --porcelain)" ]; then + >&2 echo "error: working tree is not clean" + exit 2 +fi + +# Read the current version from pyproject.toml. +CURRENT_VERSION=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version) + +# Expected shape: X.Y.0.devN +if ! [[ "${CURRENT_VERSION}" =~ ^([0-9]+)\.([0-9]+)\.0\.dev([0-9]+)$ ]]; then + >&2 echo "error: pyproject version ${CURRENT_VERSION} does not match X.Y.0.devN" + exit 2 +fi +MAJOR="${BASH_REMATCH[1]}" +MINOR="${BASH_REMATCH[2]}" + +if [ -n "${CONFIRM_MINOR:-}" ]; then + if [ "${CONFIRM_MINOR}" != "${MAJOR}.${MINOR}" ]; then + >&2 echo "error: CONFIRM_MINOR=${CONFIRM_MINOR} does not match pyproject minor ${MAJOR}.${MINOR}" + exit 2 + fi +fi + +RELEASE_BRANCH="release/v${MAJOR}.${MINOR}" +RC_VERSION="${MAJOR}.${MINOR}.0rc0" +RC_TAG="v${RC_VERSION}" +NEXT_MINOR=$((MINOR + 1)) +NEXT_DEV_VERSION="${MAJOR}.${NEXT_MINOR}.0.dev0" + +# Refuse if tag or branch already exists (local or remote). +git fetch origin --tags --prune +if git rev-parse --verify "refs/tags/${RC_TAG}" >/dev/null 2>&1; then + >&2 echo "error: tag ${RC_TAG} already exists" + exit 2 +fi +if git rev-parse --verify "refs/heads/${RELEASE_BRANCH}" >/dev/null 2>&1 \ + || git rev-parse --verify "refs/remotes/origin/${RELEASE_BRANCH}" >/dev/null 2>&1; then + >&2 echo "error: branch ${RELEASE_BRANCH} already exists" + exit 2 +fi + +git config --global user.name 'github-actions[bot]' +git config --global user.email 'github-actions[bot]@users.noreply.github.com' + +# Create the release branch and set the rc version there. +git checkout -b "${RELEASE_BRANCH}" +uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "${RC_VERSION}" +UV_FROZEN=0 uv lock --upgrade-package mellea +git add pyproject.toml uv.lock +git commit -m "release: cut v${MAJOR}.${MINOR} branch at ${RC_VERSION} [skip ci]" +git push origin "${RELEASE_BRANCH}" + +# Publish rc0 via release.sh — tag-only when PUBLISH_PRERELEASES is disabled, +# full prerelease flow when enabled. +RELEASE_BRANCH="${RELEASE_BRANCH}" "$(dirname "$0")/release.sh" + +# Back to main and bump to the next dev version. +git checkout main +uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "${NEXT_DEV_VERSION}" +UV_FROZEN=0 uv lock --upgrade-package mellea +git add pyproject.toml uv.lock +git commit -m "chore: bump main to ${NEXT_DEV_VERSION} [skip ci]" +git push origin main + +set +x +echo "" +echo "Cut ${RELEASE_BRANCH} at ${RC_VERSION}" +echo "Bumped main to ${NEXT_DEV_VERSION}" +echo "" +echo "Next step: dispatch the Publish release workflow against ${RELEASE_BRANCH} with bump_type=rc to produce the next rc (rc1)." diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh index 55f8295af..c40aba08a 100755 --- a/.github/scripts/release.sh +++ b/.github/scripts/release.sh @@ -1,50 +1,194 @@ #!/bin/bash +# Publish a release from the currently checked-out branch. Uses the version +# already written to pyproject.toml; does not modify it. +# +# Env: +# RELEASE_BRANCH (required) — branch being published (e.g. release/v0.6). +# Guards against accidentally releasing from main. +# GH_TOKEN (required) — for gh release create / gh pr create +# GITHUB_REPOSITORY (required) — owner/repo, used for origin URL and links +# CHGLOG_FILE (optional) — path to changelog (default: CHANGELOG.md) +# ALLOW_MAIN_RELEASE=1 (optional) — bypass the main-branch guard +# PUBLISH_PRERELEASES (optional) — "true" to tag prereleases and upload to +# PyPI; default "false" leaves the bump +# commit as the only artifact. +# +# Prereleases (rc, .dev): when PUBLISH_PRERELEASES=true, push a git tag, +# create a prerelease GitHub Release with notes diffed against the previous +# tag (incremental), and dispatch pypi.yml. Otherwise no tag, no Release, +# no PyPI upload — the version-bump commit on the branch is the only +# artifact. +# +# Finals: create a GitHub Release (tag + Release object + generated notes +# diffed against the previous final), append to the changelog on the release +# branch, and open a sync PR to main with the changelog delta. +# +# All write steps are idempotent so `bump_type=none` retries are safe after +# a partial failure. -set -e # trigger failure on error - do not remove! -set -x # display command on output +set -euo pipefail +set -x -if [ -z "${TARGET_VERSION}" ]; then - >&2 echo "No TARGET_VERSION specified" - exit 1 +if [ -z "${RELEASE_BRANCH:-}" ]; then + >&2 echo "error: RELEASE_BRANCH env var is required" + exit 2 fi +if [ "${RELEASE_BRANCH}" = "main" ] && [ "${ALLOW_MAIN_RELEASE:-0}" != "1" ]; then + >&2 echo "error: refusing to release from main; dispatch against a release/v* branch" + exit 2 +fi + CHGLOG_FILE="${CHGLOG_FILE:-CHANGELOG.md}" +PUBLISH_PRERELEASES="${PUBLISH_PRERELEASES:-false}" + +TARGET_VERSION=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version) +TARGET_TAG_NAME="v${TARGET_VERSION}" -# update package version -uvx --from=toml-cli toml set --toml-path=pyproject.toml project.version "${TARGET_VERSION}" -UV_FROZEN=0 uv lock --upgrade-package mellea +IS_PRERELEASE=0 +if [[ "${TARGET_VERSION}" == *rc* ]] || [[ "${TARGET_VERSION}" == *.dev* ]]; then + IS_PRERELEASE=1 +fi + +# START_TAG drives --notes-start-tag. Prereleases and finals serve +# different audiences and use different selection rules: +# - Prereleases (rc, dev): incremental — most recent reachable tag +# from HEAD. Steady state: previous rc on the release branch, previous +# dev on main. First-in-cycle (no reachable prior tag) leaves START_TAG +# empty so gh's default fills in. Testers want "what's new in rc2 +# since rc1"; the cumulative view lives on the final's Release. +# - Finals: cumulative — previous final regardless of intermediate +# prereleases. Selection by version shape: +# * Patch (Z>0): git describe excluding rc/dev (reachable on this +# branch; excludes a parallel-branch v0.7.0). +# * Minor (Z=0, Y>0): previous final is on a parallel release branch +# and isn't reachable; pick highest v(M).(Y-1).* by semver. +# * First minor (X.0.0): empty → gh's default ("latest non-prerelease"). +START_TAG="" +if [ "${IS_PRERELEASE}" = "1" ]; then + START_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || true) +elif [[ "${TARGET_VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + TGT_MAJOR="${BASH_REMATCH[1]}" + TGT_MINOR="${BASH_REMATCH[2]}" + TGT_PATCH="${BASH_REMATCH[3]}" + if [ "${TGT_PATCH}" -gt 0 ]; then + START_TAG=$(git describe --tags --abbrev=0 \ + --match 'v*' --exclude '*rc*' --exclude '*.dev*' \ + HEAD 2>/dev/null || true) + elif [ "${TGT_MINOR}" -gt 0 ]; then + PREV_MINOR=$((TGT_MINOR - 1)) + START_TAG=$(git tag -l "v${TGT_MAJOR}.${PREV_MINOR}.*" \ + | grep -vE '(rc|\.dev)' \ + | sort -V \ + | tail -1) + fi +fi -# push changes git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' -# Configure the remote with the token git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" -TARGET_TAG_NAME="v${TARGET_VERSION}" +if [ "${IS_PRERELEASE}" = "1" ]; then + if [ "${PUBLISH_PRERELEASES}" = "true" ]; then + if ! git rev-parse "refs/tags/${TARGET_TAG_NAME}" >/dev/null 2>&1; then + git tag "${TARGET_TAG_NAME}" + git push origin "${TARGET_TAG_NAME}" + fi + if ! gh release view "${TARGET_TAG_NAME}" >/dev/null 2>&1; then + if [ -n "${START_TAG}" ]; then + gh release create "${TARGET_TAG_NAME}" \ + --target "${RELEASE_BRANCH}" \ + --prerelease \ + --generate-notes \ + --notes-start-tag "${START_TAG}" + else + gh release create "${TARGET_TAG_NAME}" \ + --target "${RELEASE_BRANCH}" \ + --prerelease \ + --generate-notes + fi + fi + gh workflow run pypi.yml --ref "${TARGET_TAG_NAME}" + echo "Published prerelease ${TARGET_TAG_NAME} (tag + Release + PyPI)" + else + echo "Prerelease ${TARGET_TAG_NAME}: PUBLISH_PRERELEASES=false, skipping tag and PyPI upload" + fi + exit 0 +fi -# Commit and push version bump first so the tag has the right base -git add pyproject.toml uv.lock -COMMIT_MSG="chore: bump version to ${TARGET_VERSION} [skip ci]" -git commit -m "${COMMIT_MSG}" -git push origin main +if ! gh release view "${TARGET_TAG_NAME}" >/dev/null 2>&1; then + if [ -n "${START_TAG}" ]; then + gh release create "${TARGET_TAG_NAME}" \ + --target "${RELEASE_BRANCH}" \ + --generate-notes \ + --notes-start-tag "${START_TAG}" + else + gh release create "${TARGET_TAG_NAME}" \ + --target "${RELEASE_BRANCH}" \ + --generate-notes + fi +fi -# create GitHub release (incl. Git tag) with GitHub-native generated notes -gh release create "${TARGET_TAG_NAME}" --generate-notes +# Dispatch follow-on workflows directly (GITHUB_TOKEN-authored release/tag +# events don't auto-trigger). +gh workflow run pypi.yml --ref "${TARGET_TAG_NAME}" +gh workflow run docs-publish.yml --field "release_tag=${TARGET_TAG_NAME}" + +if [ "${RELEASE_BRANCH}" = "main" ]; then + echo "Published ${TARGET_TAG_NAME} from main — skipping changelog sync PR" + exit 0 +fi -# pull the generated notes back locally to update the changelog REL_NOTES=$(mktemp) gh release view "${TARGET_TAG_NAME}" --json body -q ".body" >> "${REL_NOTES}" -# update changelog TMP_CHGLOG=$(mktemp) RELEASE_URL="$(gh repo view --json url -q ".url")/releases/tag/${TARGET_TAG_NAME}" -printf "## [${TARGET_TAG_NAME}](${RELEASE_URL}) - $(date -Idate)\n\n" >> "${TMP_CHGLOG}" +printf "## [%s](%s) - %s\n\n" "${TARGET_TAG_NAME}" "${RELEASE_URL}" "$(date -Idate)" >> "${TMP_CHGLOG}" cat "${REL_NOTES}" >> "${TMP_CHGLOG}" if [ -f "${CHGLOG_FILE}" ]; then printf "\n" | cat - "${CHGLOG_FILE}" >> "${TMP_CHGLOG}" fi mv "${TMP_CHGLOG}" "${CHGLOG_FILE}" +# idempotent: skip if a prior run committed the same content. +git add "${CHGLOG_FILE}" +if ! git diff --cached --quiet; then + git commit -m "docs: update changelog for ${TARGET_TAG_NAME} [skip ci]" + git push origin "${RELEASE_BRANCH}" +fi + +# Main is never pushed to directly from this script; branch protection +# applies normally. +SYNC_BRANCH="chore/changelog-sync-${TARGET_VERSION}" +git fetch origin main +# Reuse an existing sync branch on origin so a partial prior run's commits +# aren't silently discarded. +if git rev-parse --verify "refs/remotes/origin/${SYNC_BRANCH}" >/dev/null 2>&1; then + git checkout "${SYNC_BRANCH}" + git reset --hard "origin/${SYNC_BRANCH}" +else + git checkout -b "${SYNC_BRANCH}" origin/main +fi +git checkout "${RELEASE_BRANCH}" -- "${CHGLOG_FILE}" git add "${CHGLOG_FILE}" -git commit -m "docs: update changelog for ${TARGET_TAG_NAME} [skip ci]" -git push origin main \ No newline at end of file +if ! git diff --cached --quiet; then + git commit -m "docs: sync changelog for ${TARGET_TAG_NAME}" + git push origin "${SYNC_BRANCH}" +fi + +EXISTING_PR=$(gh pr list --head "${SYNC_BRANCH}" --base main --state open --json number --jq '.[0].number // ""') +if [ -z "${EXISTING_PR}" ]; then + gh pr create \ + --base main \ + --head "${SYNC_BRANCH}" \ + --title "docs: sync changelog for ${TARGET_TAG_NAME}" \ + --body "Automated changelog sync from \`${RELEASE_BRANCH}\` after publishing [${TARGET_TAG_NAME}](${RELEASE_URL}). + +This PR brings the release-branch CHANGELOG entry back to main so the project root CHANGELOG remains the canonical history across all branches." +fi + +# GITHUB_TOKEN-authored pull_request events don't trigger workflows, so +# required status checks on main would block the sync PR forever. Dispatch +# CI explicitly against the sync branch. +gh workflow run ci.yml --ref "${SYNC_BRANCH}" diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index 4dde1df71..000000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: "Run CD" - -on: - workflow_dispatch: - inputs: - force_release: - description: 'Force release even if previous checks fail.' - type: boolean - required: false - default: false - -permissions: {} - -env: - UV_FROZEN: "1" - CICD: 1 - -jobs: - code-checks: - permissions: - contents: read - uses: ./.github/workflows/quality.yml - # with: - # push_coverage: false - pre-release-check: - permissions: - contents: read - runs-on: ubuntu-latest - outputs: - TARGET_TAG_V: ${{ steps.version_check.outputs.TRGT_VERSION }} - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - fetch-depth: 0 # for fetching tags, required for semantic-release - persist-credentials: false - - name: Install uv and set the python version - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - with: - enable-cache: true - - name: Install dependencies - run: uv sync --only-dev - - name: Check version of potential release - id: version_check - run: | - TRGT_VERSION=$(uv run --no-sync semantic-release print-version) - echo "TRGT_VERSION=${TRGT_VERSION}" >> "$GITHUB_OUTPUT" - echo "${TRGT_VERSION}" - - name: Check notes of potential release - run: uv run --no-sync semantic-release changelog --unreleased - release: - permissions: - contents: write - needs: [code-checks, pre-release-check] - # Run this job only if the `TARGET_TAG_V` is set AND (the previous jobs - # were successful OR we are forcing the release). - if: >- - ${{ needs.pre-release-check.outputs.TARGET_TAG_V != '' && - ( success() || inputs.force_release ) }} - environment: auto-release - runs-on: ubuntu-latest - concurrency: release - steps: - - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 - id: app-token - with: - app-id: ${{ vars.CI_APP_ID }} - private-key: ${{ secrets.CI_PRIVATE_KEY }} - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - token: ${{ steps.app-token.outputs.token }} - fetch-depth: 0 # for fetching tags, required for semantic-release - persist-credentials: false - - name: Install uv and set the python version - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - with: - enable-cache: true - - name: Install dependencies - run: uv sync --only-dev - - name: Run release script - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - TARGET_VERSION: ${{ needs.pre-release-check.outputs.TARGET_TAG_V }} - CHGLOG_FILE: CHANGELOG.md - GITHUB_REPOSITORY: ${{ github.repository }} - run: ./.github/scripts/release.sh - shell: bash diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20d99d9d8..48da3aa66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,14 @@ on: pull_request: types: [opened, reopened, synchronize] merge_group: + push: + branches: ['release/**'] + workflow_dispatch: # used by release.sh for sync-PR CI (anti-loop workaround) + inputs: + ref: + description: 'Branch or ref to run CI against' + type: string + required: false permissions: {} diff --git a/.github/workflows/cut-release-branch.yml b/.github/workflows/cut-release-branch.yml new file mode 100644 index 000000000..4990e17d2 --- /dev/null +++ b/.github/workflows/cut-release-branch.yml @@ -0,0 +1,58 @@ +name: "Cut release branch" + +# Creates a release/vX.Y branch from main and bumps main to the next minor's +# .dev0. See RELEASE.md for the full workflow. +# +# Must be dispatched from the GitHub Actions UI. `github-actions[bot]` needs +# bypass rights on main's branch-protection ruleset to push the main-side +# dev bump. + +on: + workflow_dispatch: + inputs: + confirm_minor: + description: >- + Optional safety check: if set, must match the minor derived from + pyproject.toml on main (e.g. "0.6"). Leave blank to trust pyproject. + type: string + required: false + default: "" + +permissions: {} + +env: + UV_FROZEN: "1" + CICD: 1 + +jobs: + cut: + environment: auto-release + permissions: + contents: write # push main + new release branch + pull-requests: write # release.sh may open changelog sync PR if rc0 is published (flag on) + actions: write # release.sh dispatches pypi.yml after tagging rc0 + runs-on: ubuntu-latest + concurrency: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + persist-credentials: true + - name: Install uv and set the python version + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --only-dev + - name: Cut release branch + env: + CONFIRM_MINOR: ${{ inputs.confirm_minor }} + # Env vars for the inline rc0 release-publish step: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + CHGLOG_FILE: CHANGELOG.md + PUBLISH_PRERELEASES: ${{ vars.PUBLISH_PRERELEASES || 'false' }} + run: ./.github/scripts/cut_release_branch.sh + shell: bash diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml index ecba33cc0..5b48e9f49 100644 --- a/.github/workflows/docs-publish.yml +++ b/.github/workflows/docs-publish.yml @@ -41,6 +41,12 @@ on: description: "Fail the build if validation checks fail" type: boolean default: false + release_tag: + description: >- + Set to a release tag (e.g. `v0.6.0`) to run the + deploy-on-release flow. Used by release.sh. + type: string + default: "" permissions: {} @@ -315,13 +321,15 @@ jobs: contents: write timeout-minutes: 10 - # Deploy on: push to main, release, force_publish via dispatch, - # or PRs labelled "docs-preview" (→ docs/preview branch). + # Deploy on: push to main, release event, force_publish or release_tag + # via dispatch, or PRs labelled "docs-preview" (→ docs/preview branch). + # The runtime latest-final-by-semver check below prevents older-branch + # patches from overwriting production docs. if: >- github.event_name == 'push' || github.event_name == 'release' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'docs-preview')) || - (github.event_name == 'workflow_dispatch' && inputs.force_publish) + (github.event_name == 'workflow_dispatch' && (inputs.force_publish || inputs.release_tag != '')) steps: - name: Download docs artifact @@ -330,10 +338,64 @@ jobs: name: docs-site path: docs-site/ + # For release events, confirm the published tag is the latest final by + # semver across all release branches. Without this, publishing v0.6.1 + # after v0.7.0 already exists would incorrectly downgrade docs/production + # to the older minor's docs. + - name: Check release is latest final + id: latest_check + # Fail open: deploy if the check succeeded (true) or a transient API + # error left the output empty. Only skip on an explicit 'false'. + continue-on-error: true + if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CURRENT_TAG: ${{ github.event.release.tag_name || inputs.release_tag }} + REPO: ${{ github.repository }} + run: | + python3 - <<'PY' + import json, os, subprocess, sys + from packaging.version import Version, InvalidVersion + + current = os.environ["CURRENT_TAG"] + repo = os.environ["REPO"] + raw = subprocess.check_output( + ["gh", "api", f"repos/{repo}/releases", "--paginate", + "--jq", ".[] | select(.prerelease==false and .draft==false) | .tag_name"], + text=True, + ) + candidates = [] + for line in raw.splitlines(): + t = line.strip() + if not t: + continue + try: + candidates.append((Version(t.lstrip("v")), t)) + except InvalidVersion: + continue + + try: + current_version = Version(current.lstrip("v")) + except InvalidVersion: + print(f"error: current tag {current} is not a valid version") + sys.exit(1) + + # include the current release itself (the API may not include it + # yet if eventing races with the release event) + candidates.append((current_version, current)) + latest_version, latest_tag = max(candidates) + is_latest = (latest_tag == current or latest_version == current_version) + out = os.environ["GITHUB_OUTPUT"] + with open(out, "a") as fh: + fh.write(f"is_latest_final={'true' if is_latest else 'false'}\n") + fh.write(f"latest_tag={latest_tag}\n") + print(f"current={current} latest={latest_tag} is_latest={is_latest}") + PY + - name: Determine target branch id: target run: | - if [ "$EVENT_NAME" = "release" ]; then + if [ "$EVENT_NAME" = "release" ] || [ -n "${INPUTS_RELEASE_TAG}" ]; then echo "branch=docs/production" >> "$GITHUB_OUTPUT" elif [ "$EVENT_NAME" = "pull_request" ]; then echo "branch=docs/preview" >> "$GITHUB_OUTPUT" @@ -345,6 +407,7 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} INPUTS_TARGET_BRANCH: ${{ inputs.target_branch }} + INPUTS_RELEASE_TAG: ${{ inputs.release_tag }} - name: Add DO NOT EDIT warning run: | @@ -365,6 +428,13 @@ jobs: EOF - name: Deploy to ${{ steps.target.outputs.branch }} + # Skip production deploy when a release is not the latest final by + # semver (e.g. a patch on an older release branch). The latest_check + # step runs for both `release` events and workflow_dispatch with + # release_tag set; either path can request the skip. + if: >- + steps.latest_check.conclusion == 'skipped' || + steps.latest_check.outputs.is_latest_final != 'false' uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-dev-from-main.yml b/.github/workflows/publish-dev-from-main.yml new file mode 100644 index 000000000..2ec1a8d96 --- /dev/null +++ b/.github/workflows/publish-dev-from-main.yml @@ -0,0 +1,81 @@ +name: "Publish dev release from main" + +# Iterates main's .devN counter and (optionally) publishes a development +# release. Intended for ad-hoc / case-by-case dev tags — e.g. when a +# contributor wants a tagged snapshot of main for debugging or external +# testing. Only tags and the .devN counter advance unless +# PUBLISH_PRERELEASES=true on the repo, in which case a GitHub release + +# PyPI upload happen too. +# +# See RELEASE.md for the full release-process documentation. + +on: + workflow_dispatch: {} + +permissions: {} + +env: + UV_FROZEN: "1" + CICD: 1 + +jobs: + guard: + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Require main + env: + REF_NAME: ${{ github.ref_name }} + run: | + if [ "${REF_NAME}" != "main" ]; then + echo "error: publish-dev-from-main must run against main; got ${REF_NAME}" + exit 2 + fi + publish: + environment: auto-release + permissions: + contents: write # push tag + bump commit to main + actions: write # release.sh dispatches pypi.yml after tagging + needs: [guard] + runs-on: ubuntu-latest + concurrency: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: main + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + persist-credentials: false + - name: Install uv and set the python version + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --only-dev + - name: Configure git user + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + # Publish-then-increment: first tag/release main's current .devN, then + # advance pyproject to .dev(N+1) so the next dispatch is set up. This + # makes "dev release main as it is right now" match the pyproject state + # the operator sees when inspecting the branch. + - name: Publish current .devN + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_BRANCH: main + ALLOW_MAIN_RELEASE: "1" + CHGLOG_FILE: CHANGELOG.md + GITHUB_REPOSITORY: ${{ github.repository }} + PUBLISH_PRERELEASES: ${{ vars.PUBLISH_PRERELEASES || 'false' }} + run: ./.github/scripts/release.sh + shell: bash + - name: Advance .devN counter on main + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + uv run --no-sync python .github/scripts/bump_version.py --mode dev + git push origin main diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 000000000..b0a153dc0 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,145 @@ +name: "Publish release" + +# Publishes a release from a release/v* branch. Operator picks a bump_type +# to advance the version (rc, final, patch-rc, patch-final) or re-publish +# whatever is currently in pyproject.toml (none). See RELEASE.md. + +on: + workflow_dispatch: + inputs: + bump_type: + description: >- + How to advance the version before publishing. Use `rc` throughout a + minor cycle, `patch-rc` throughout a patch cycle, `final` / + `patch-final` to promote an rc, or `none` to republish the + pyproject version as-is. + type: choice + options: + - rc + - final + - patch-rc + - patch-final + - none + required: true + default: rc + force_release: + description: 'Force release even if previous checks fail.' + type: boolean + required: false + default: false + allow_main_release: + description: 'Override the main-branch guard. Leave unchecked.' + type: boolean + required: false + default: false + +permissions: {} + +env: + UV_FROZEN: "1" + CICD: 1 + +jobs: + guard: + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Refuse to release from main + env: + REF_NAME: ${{ github.ref_name }} + ALLOW: ${{ inputs.allow_main_release }} + run: | + if [ "${REF_NAME}" = "main" ] && [ "${ALLOW}" != "true" ]; then + echo "error: refusing to release from main." + echo "Dispatch this workflow against a release/v* branch instead." + echo "See RELEASE.md for the release-branch workflow." + exit 2 + fi + code-checks: + permissions: + contents: read + uses: ./.github/workflows/quality.yml + pre-release-check: + permissions: + contents: read + needs: [guard] + runs-on: ubuntu-latest + outputs: + TARGET_TAG_V: ${{ steps.version_check.outputs.TRGT_VERSION }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install uv and set the python version + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --only-dev + - name: Compute target version + id: version_check + env: + BUMP_TYPE: ${{ inputs.bump_type }} + run: | + if [ "${BUMP_TYPE}" = "none" ]; then + TRGT_VERSION=$(uvx --from=toml-cli toml get --toml-path=pyproject.toml project.version) + else + TRGT_VERSION=$(uv run --no-sync python .github/scripts/bump_version.py \ + --mode "${BUMP_TYPE}" --dry-run) + fi + echo "TRGT_VERSION=${TRGT_VERSION}" >> "$GITHUB_OUTPUT" + echo "Target version: ${TRGT_VERSION}" + release: + permissions: + contents: write # push tags, branches, commits + pull-requests: write # release.sh opens changelog sync PR for finals + actions: write # release.sh dispatches pypi.yml and docs-publish.yml + needs: [guard, code-checks, pre-release-check] + # Run only if a target version was computed AND (previous jobs succeeded + # OR the operator set force_release). + if: >- + ${{ needs.pre-release-check.outputs.TARGET_TAG_V != '' && + ( success() || inputs.force_release ) }} + environment: auto-release + runs-on: ubuntu-latest + concurrency: release + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ github.ref_name }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + persist-credentials: false + - name: Install uv and set the python version + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --only-dev + - name: Configure git user + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email 'github-actions[bot]@users.noreply.github.com' + - name: Bump version on release branch + if: inputs.bump_type != 'none' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + BUMP_TYPE: ${{ inputs.bump_type }} + REF_NAME: ${{ github.ref_name }} + run: | + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + uv run --no-sync python .github/scripts/bump_version.py --mode "${BUMP_TYPE}" + git push origin "${REF_NAME}" + - name: Run release script + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_BRANCH: ${{ github.ref_name }} + ALLOW_MAIN_RELEASE: ${{ inputs.allow_main_release && '1' || '0' }} + CHGLOG_FILE: CHANGELOG.md + GITHUB_REPOSITORY: ${{ github.repository }} + PUBLISH_PRERELEASES: ${{ vars.PUBLISH_PRERELEASES || 'false' }} + run: ./.github/scripts/release.sh + shell: bash diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 96b5d0faf..97b3a3a2c 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,8 +1,13 @@ name: "Build and publish package" -# Test comment for a build + +# Fires on v* tag push, or via explicit dispatch (release.sh uses this when +# GITHUB_TOKEN-authored pushes can't auto-trigger). Prereleases are gated on +# the PUBLISH_PRERELEASES repo variable; finals always publish. + on: - release: - types: [published] + push: + tags: ['v*'] + workflow_dispatch: env: UV_FROZEN: "1" @@ -25,16 +30,38 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: persist-credentials: false + - name: Decide whether to publish + id: gate + env: + PUBLISH_PRERELEASES: ${{ vars.PUBLISH_PRERELEASES || 'false' }} + # Read version from pyproject.toml, not github.ref_name: on a manual + # workflow_dispatch ref_name is the branch (release/v0.6) and the + # prerelease pattern would not match, causing the gate to fail open. + run: | + VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + echo "Resolved version from pyproject.toml: ${VERSION}" + if [[ "${VERSION}" == *rc* ]] || [[ "${VERSION}" == *.dev* ]]; then + if [ "${PUBLISH_PRERELEASES}" != "true" ]; then + echo "Prerelease ${VERSION} with PUBLISH_PRERELEASES=false — skipping PyPI upload" + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + echo "skip=false" >> "$GITHUB_OUTPUT" - name: Install uv and set the python version + if: steps.gate.outputs.skip != 'true' uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: python-version: ${{ matrix.python-version }} enable-cache: false - name: Install dependencies + if: steps.gate.outputs.skip != 'true' run: uv sync --all-extras - name: Build package + if: steps.gate.outputs.skip != 'true' run: uv build - name: Publish distribution 📦 to PyPI + if: steps.gate.outputs.skip != 'true' uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: attestations: true diff --git a/RELEASE.md b/RELEASE.md index baeb246ac..340445c77 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,66 +1,252 @@ # RELEASE.md -## Overview -This document describes the release process for the project. It outlines how release candidates are prepared, how testing is performed, how the final package is published, and what conventions the project uses for versioning and communication. The goal is to ensure reliability, transparency, and predictability for users and contributors. +Mellea uses a release-branch workflow. Each minor release lives on a +long-lived `release/vX.Y` branch that carries the final release and any +later patches. `main` carries `.devN`-versioned work for the next minor. +Releases target a roughly 4-week cadence; patch releases happen as needed. -## Release Cadence -Releases are prepared weekly, with work occurring roughly on the following schedule: +- [Making a minor release](#making-a-minor-release) +- [Making a patch release](#making-a-patch-release) +- [Troubleshooting](#troubleshooting) +- [Appendix](#appendix) -- **Mon–Tue:** Identify and finalize high‑priority pull requests for inclusion. -- **Wed:** Merge selected pull requests, lock down changes, and cut the release. +## Making a minor release -This schedule may shift depending on team availability, urgency of fixes, and the stability of the codebase. +1. [Cut the release branch](#1-cut-the-release-branch) +2. [Stabilize on the release branch](#2-stabilize-on-the-release-branch) +3. [Publish the final](#3-publish-the-final) +4. [Sync the changelog back to main](#4-sync-the-changelog-back-to-main) + +### 1. Cut the release branch + +Run [Cut release branch](https://github.com/generative-computing/mellea/actions/workflows/cut-release-branch.yml) +against `main`. + +- **Optional**: enter the expected `X.Y` (e.g. `0.6`) in `confirm_minor` as + a sanity check. Leave blank to trust whatever is in `pyproject.toml`. + +After this runs: `release/vX.Y` exists at `X.Y.0rc0`, and `main` has been +bumped to `X.(Y+1).0.dev0`. + +### 2. Stabilize on the release branch + +Land fixes via PRs targeting `release/vX.Y` (normal review + CI; protection +mirrors `main`). If the same change also belongs on `main`, open a +follow-up PR to `main` once the release-branch PR is merged. + +> RC iteration during stabilization (`bump_type: rc`) is currently a no-op: +> the rc counter advances in `pyproject.toml` but nothing is published. +> See [rc cycling](#rc-cycling) in the appendix for what changes when +> prereleases are enabled. + +### 3. Publish the final + +Run [Publish release](https://github.com/generative-computing/mellea/actions/workflows/publish-release.yml) +against `release/vX.Y`. + +- `bump_type`: `final` + +After this runs: `vX.Y.0` is tagged, the GitHub Release exists, the PyPI +upload is done, and docs/production has been redeployed. + +### 4. Sync the changelog back to main + +The publish workflow opens a PR titled `docs: sync changelog for vX.Y.0` +from `chore/changelog-sync-X.Y.0` to `main`. Review and merge it. + +## Making a patch release + +1. [Land fixes on the release branch](#1-land-fixes-on-the-release-branch) +2. [Publish the patch](#2-publish-the-patch) +3. [Sync the changelog back to main](#3-sync-the-changelog-back-to-main) + +### 1. Land fixes on the release branch + +PR targeting `release/vX.Y`. If the change also belongs on `main`, open a +follow-up PR to `main` once the release-branch PR is merged. + +### 2. Publish the patch + +Run [Publish release](https://github.com/generative-computing/mellea/actions/workflows/publish-release.yml) +against `release/vX.Y`. + +- `bump_type`: `patch-final` + +> Patch rc iteration (`bump_type: patch-rc`) is currently a no-op for the +> same reason as minor rc iteration; see [rc cycling](#rc-cycling). + +### 3. Sync the changelog back to main + +Same as the minor flow: review and merge the auto-opened +`chore/changelog-sync-X.Y.Z` PR. + +## Troubleshooting + +### Retry a failed publish + +`bump_type: none` re-runs `publish-release` against whatever version is +already in `pyproject.toml`, skipping the version-bump step. Useful when a +previous run failed after the bump committed but before the publish +completed. + +The retry is a "skip what's done, finish what isn't" path — it does not +validate existing artifacts. If a prior run produced a tag at the wrong +commit, a Release with stale notes, or a sync PR with a stale body, delete +the bad artifact (`gh release delete`, `git push --delete`, `gh pr close`) +before re-dispatching. Retry is for resuming after a partial failure, not +for fixing a corrupted result. + +### Cutting a major release + +The cut-release workflow always bumps `main` by one minor. To cut a major +(e.g. `1.0.0` from a `0.x` line): + +1. Land a regular PR on `main` setting `pyproject.toml` to + `(X+1).0.0.dev0`. +2. Dispatch [Cut release branch](https://github.com/generative-computing/mellea/actions/workflows/cut-release-branch.yml) + as usual. + +### Ad-hoc dev publish from main + +Used when someone needs a tagged point-in-time artifact of `main` outside +the normal release cadence. With `PUBLISH_PRERELEASES=false` (the default) +this workflow is a no-op for publishing; the only effect is that `main`'s +`.devN` counter advances. + +Run [Publish dev release from main](https://github.com/generative-computing/mellea/actions/workflows/publish-dev-from-main.yml) +against `main`. With the flag enabled, the workflow tags `main` HEAD, +creates a prerelease GitHub Release, and uploads to PyPI. Either way, it +then bumps `main` from `X.Y.Z.devN` to `X.Y.Z.dev(N+1)` and pushes. + +`main`'s `pyproject.toml` always reflects the version that the next +dispatch will publish. + +--- + +# Appendix ## Versioning -This project follows **[Semantic Versioning (semver)](https://semver.org)**. Breaking changes must be clearly documented and highlighted in release notes. - -To see the current, **prospective** version that will be published by the next release action, you can run: -``` -uv run --no-sync semantic-release print-version -``` - -## Pre‑Release Coordination -### 1. Identify Candidate Pull Requests -Early in the week, maintainers review open pull requests and determine which ones should be included in the upcoming release. - -### 2. Merge or Defer Selected Pull Requests -By mid‑week, all targeted PRs should be merged or explicitly deferred. No new PRs should be merged once the release process begins. - -## Release Process -### 1. Freeze `main` -Once the release process begins, no new PRs may be merged into `main`. - -### 2. Run the Full Test Suite -All tests must pass before proceeding. The suite includes unit tests, integration tests, and example workflows. Issues are classified into quick fixes or larger problems requiring release delay. This should be run against python versions 3.11, 3.12, and 3.13. - -### 3. Trigger Release Automation -A GitHub Actions workflow handles the CI pipeline, branch cutting, and publishing to PyPI. - -## Post‑Release Steps -### 1. Announcement & Communication -When a release includes noteworthy updates—especially breaking API changes—maintainers should publish release notes and communicate updates publicly. - -### 2. Documentation Synchronization -Documentation must be updated before the release is finalized. API Documentation is automatically generated. - -### 3. Transparency: Test Strategy & Results -Testing methodology and coverage information may be made public where appropriate. - -## Release Types -- **Fix releases** – bug fixes and stability improvements -- **Feature releases** – new capabilities, backward compatible -- **Breaking releases** – major API revisions - -## Supported Use Cases & Stability -The project is in **beta**, so users should expect rapid iteration and occasional breaking changes. - -## Future Improvements -Future enhancements may include: -- add package dependency tests - - ensure each piece of mellea can run with only its necessary mellea package (ie huggingface with mellea[hf]) -- release candidate branch - - the job supports a branch selector; we should make sure it works as expected - - this also means we need to keep track of which PRs need to be double merged (ie into both the release and main branches) -- improved PR coordination - - this becomes less necessary with a candidate release branch -- publishing a standardized test strategy + +Versions follow **[PEP 440](https://peps.python.org/pep-0440/)** (which is +compatible with SemVer for final releases). + +| Phase | Branch | Version example | Tag | +|-------|--------|-----------------|-----| +| Dev on main | `main` | `0.6.0.dev0` | (untagged) | +| Release branch cut | `release/v0.6` | `0.6.0rc0` | `v0.6.0rc0` if `PUBLISH_PRERELEASES`, else untagged | +| Further RCs | `release/v0.6` | `0.6.0rc1`, `rc2`, … | `v0.6.0rcN` if `PUBLISH_PRERELEASES`, else untagged | +| Final X.Y release | `release/v0.6` | `0.6.0` | `v0.6.0` | +| Patch RC | `release/v0.6` | `0.6.1rc0` | `v0.6.1rc0` if `PUBLISH_PRERELEASES`, else untagged | +| Patch final | `release/v0.6` | `0.6.1` | `v0.6.1` | +| Next dev on main | `main` | `0.7.0.dev0` | (untagged) | + +Invariants: + +- `main` always carries `X.Y.0.devN`. `main` is tagged only when a dev + publication runs; never during routine commits. +- Release branches always carry `X.Y.Zrc?` or `X.Y.Z`. +- Prereleases (`rcN`, `.devN`) are tagged, uploaded to PyPI, and given a + prerelease-marked GitHub Release only when + [`PUBLISH_PRERELEASES`](#the-publish_prereleases-flag) is `true`. With the + default (`false`), the version bump commits to the branch but no tag, no + PyPI upload, and no Release. Prerelease Releases use `--prerelease` so + they don't appear as "latest" on GitHub. + +## Workflow inventory + +| Workflow | Purpose | +|----------|---------| +| [`cut-release-branch`](.github/workflows/cut-release-branch.yml) | Cut `release/vX.Y` from `main`, publish `X.Y.0rc0`, bump `main` to the next `.dev0` | +| [`publish-release`](.github/workflows/publish-release.yml) | Publish a release (rc, final, patch-rc, patch-final, or retry a failed publish) | +| [`publish-dev-from-main`](.github/workflows/publish-dev-from-main.yml) | Iterate main's `.devN` counter and (when enabled) publish a dev release | + +All three are `workflow_dispatch`-only and run from the GitHub Actions UI. + +Whether any given prerelease (`rc`, `dev`) produces a PyPI artifact depends +on the [`PUBLISH_PRERELEASES`](#the-publish_prereleases-flag) flag. + +## Branch protection + +All three write-capable workflows authenticate via `secrets.GITHUB_TOKEN`, +declaring scopes via inline `permissions:` blocks. The `GITHUB_TOKEN` +identity is `github-actions[bot]`, configured as a bypass actor on the +`main` and `release/**` rulesets so workflows can push directly: + +- `main`: `cut-release-branch` pushes the `X.(Y+1).0.dev0` bump; + `publish-dev-from-main` pushes the `.dev(N+1)` advance commit. +- `release/**`: `publish-release` pushes the version-bump commit and (for + finals) the changelog update. + +The `release/**` ruleset otherwise mirrors `main`: PR review required, +status checks (CI) required, no force-push, no deletion. + +## Release branch retention + +**Release branches are never deleted.** GitHub Releases pin to specific +commits on each branch, so pruning a branch would orphan those references +and break `git checkout v0.4.2` semantics. Old `release/v0.3`, +`release/v0.4`, etc. stay around indefinitely. + +## Docs behavior by release type + +Docs publishing (`docs-publish.yml`) deploys to `docs/production` only when +a published GitHub Release is the latest final by semver, so older-branch +patches don't overwrite production docs. + +| Release type | docs/production | docs/staging | +|--------------|-----------------|--------------| +| RC (`v0.6.0rc0`) | unchanged | unchanged | +| Final X.Y release (`v0.6.0`) | deployed | (main-push rebuilds as usual) | +| Patch on latest X.Y (`v0.6.1` after `v0.6.0`) | deployed | unchanged | +| Patch on older X.Y (`v0.5.1` after `v0.6.0`) | unchanged | unchanged | + +## RC cycling + +`bump_type: rc` (minor) and `bump_type: patch-rc` (patch) iterate the rc +counter on the release branch: + +- **`PUBLISH_PRERELEASES=false`** (current default): the rc counter + advances in `pyproject.toml`, but no tag is pushed, no Release is + created, and nothing reaches PyPI. The rc step is effectively a no-op + during stabilization, so the documented flow above goes straight from + cut to final. +- **`PUBLISH_PRERELEASES=true`** (future): each `rc` dispatch tags + `vX.Y.ZrcN`, creates a `--prerelease` GitHub Release with notes diffed + against the previous rc, and uploads to PyPI. + +When `PUBLISH_PRERELEASES` is enabled, the rc step is a real publish and +fits between [Stabilize](#2-stabilize-on-the-release-branch) and +[Publish the final](#3-publish-the-final): dispatch +[Publish release](https://github.com/generative-computing/mellea/actions/workflows/publish-release.yml) with +`bump_type: rc` (or `patch-rc`) as many times as needed. + +## The `PUBLISH_PRERELEASES` flag + +`PUBLISH_PRERELEASES` is a forward-looking feature flag. It exists so the +project can start publishing public prerelease artifacts later (likely +post-1.0) without a code change. It defaults to `false` and is expected to +stay `false` for the foreseeable future; the rest of this section +documents both states for when that changes. + +The flag is a repo variable that gates whether prereleases are tagged, +uploaded to PyPI, and given a GitHub Release. + +| `PUBLISH_PRERELEASES` | rc / dev | Finals | +|-----------------------|----------|--------| +| `false` (current) | version bump committed; no tag, no Release, no PyPI | tag + GitHub Release + PyPI + changelog entry + sync PR | +| `true` (future) | tag + prerelease GitHub Release + PyPI | tag + GitHub Release + PyPI + changelog entry + sync PR | + +While `false`, prereleases stay branch-local — the bump commit identifies +the version but there is no immutable tag pointer. Users who need a +specific prerelease can install from a branch SHA +(`pip install git+https://github.com/generative-computing/mellea@`). + +When the flag is eventually enabled, every rc and dev produces a +`--prerelease`-marked GitHub Release. Notes are incremental — +`0.6.0rc2` diffs against `0.6.0rc1`, so testers see "what changed in this +rc" without re-reading the full cycle. The cumulative view ("everything +in 0.6") shows up on the final's Release page, which diffs against the +previous final. Prerelease Releases never become the repo's "latest" +release. + +Finals always follow the full release flow regardless of the flag. diff --git a/pyproject.toml b/pyproject.toml index c9a1f550b..fcdb8532a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "pdm.backend" [project] name = "mellea" -version = "0.6.0" +version = "0.7.0.dev0" authors = [ { name = "Mellea Contributors", email = "melleaadmin@ibm.com" }, ] @@ -155,17 +155,12 @@ build = [ "pdm>=2.24.0", ] -release = [ - "python-semantic-release~=7.32", -] - # Convenience: includes all dev dependencies (used by default with uv run) dev = [ {include-group = "lint"}, {include-group = "test"}, {include-group = "typecheck"}, {include-group = "build"}, - {include-group = "release"}, "griffe>=2.0.0", "mdxify>=0.2.37", "mellea[server]", @@ -289,6 +284,7 @@ split-on-trailing-comma = false "test/**/*.py" = ["E402", "D"] "cli/**/*.py" = ["D"] "tooling/**/*.py" = ["D"] +".github/scripts/**/*.py" = ["D"] # ----------------------------- # MyPy - Type Checking @@ -408,23 +404,5 @@ exclude_lines = [ [tool.coverage.html] directory = "htmlcov" -# ----------------------------- -# Semantic Release - Versioning -# ----------------------------- - -[tool.semantic_release] -# for default values check: -# https://github.com/python-semantic-release/python-semantic-release/blob/v7.32.2/semantic_release/defaults.cfg - -version_source = "tag_only" -branch = "main" -major_on_zero = false - -# configure types which should trigger minor and patch version bumps respectively -# (note that they must be a subset of the configured allowed types): -parser_angular_allowed_types = "build,chore,ci,docs,feat,fix,perf,style,refactor,test,release" -parser_angular_minor_types = "release" -parser_angular_patch_types = "fix,perf,feat" - [tool.uv.sources] mellea = { workspace = true } diff --git a/test/scripts/test_bump_version.py b/test/scripts/test_bump_version.py new file mode 100644 index 000000000..b286c61f7 --- /dev/null +++ b/test/scripts/test_bump_version.py @@ -0,0 +1,89 @@ +"""Unit tests for `.github/scripts/bump_version.py`.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest +from packaging.version import Version + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT = REPO_ROOT / ".github" / "scripts" / "bump_version.py" + + +def _load_module(): + spec = importlib.util.spec_from_file_location("bump_version", SCRIPT) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + sys.modules["bump_version"] = mod + spec.loader.exec_module(mod) + return mod + + +bump_version = _load_module() + + +@pytest.mark.parametrize( + ("current", "mode", "expected"), + [ + ("0.6.0rc0", "rc", "0.6.0rc1"), + ("0.6.0rc5", "rc", "0.6.0rc6"), + ("0.6.0rc0", "final", "0.6.0"), + ("0.6.0rc2", "final", "0.6.0"), + ("0.6.0", "patch-rc", "0.6.1rc0"), + ("0.6.1rc0", "patch-rc", "0.6.1rc1"), + ("0.6.1rc3", "patch-rc", "0.6.1rc4"), + ("0.6.1rc0", "patch-final", "0.6.1"), + ("0.6.5rc2", "patch-final", "0.6.5"), + ("0.6.0.dev0", "dev", "0.6.0.dev1"), + ("0.7.0.dev3", "dev", "0.7.0.dev4"), + ], +) +def test_compute_next_happy_paths(current, mode, expected): + got = bump_version.compute_next(Version(current), mode) + assert str(got) == expected + + +@pytest.mark.parametrize( + ("current", "mode"), + [ + # rc requires current rc + ("0.6.0", "rc"), + # final requires current rc, not a final + ("0.6.0", "final"), + # final is for minor rcs (patch=0), not patch rcs + ("0.6.1rc0", "final"), + # patch-rc refuses minor rcs (patch=0) + ("0.6.0rc0", "patch-rc"), + # patch-final refuses minor rcs + ("0.6.0rc0", "patch-final"), + # patch-final requires current rc + ("0.6.1", "patch-final"), + # dev versions should never appear on release branches + ("0.6.0.dev0", "rc"), + ("0.6.0.dev0", "final"), + # dev mode requires a .dev release; refuses finals and rcs + ("0.6.0", "dev"), + ("0.6.0rc0", "dev"), + ], +) +def test_compute_next_rejects_disallowed(current, mode): + with pytest.raises(ValueError): + bump_version.compute_next(Version(current), mode) + + +def test_compute_next_unknown_mode(): + with pytest.raises(ValueError): + bump_version.compute_next(Version("0.6.0rc0"), "bogus") + + +def test_write_pyproject_roundtrip(tmp_path, monkeypatch): + fake = tmp_path / "pyproject.toml" + fake.write_text('[project]\nname = "x"\nversion = "0.6.0rc0"\ndependencies = []\n') + monkeypatch.setattr(bump_version, "PYPROJECT", fake) + bump_version.write_pyproject(Version("0.6.0rc1")) + assert 'version = "0.6.0rc1"' in fake.read_text() + # only the version line changed, everything else is preserved + assert 'name = "x"' in fake.read_text() diff --git a/uv.lock b/uv.lock index 4aa1ed4fb..8d1002af6 100644 --- a/uv.lock +++ b/uv.lock @@ -430,15 +430,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] -[[package]] -name = "backports-tarfile" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, -] - [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -725,19 +716,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] -[[package]] -name = "click-log" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, - { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/32/228be4f971e4bd556c33d52a22682bfe318ffe57a1ddb7a546f347a90260/click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975", size = 9985, upload-time = "2022-03-13T11:10:15.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -1312,24 +1290,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/93/0c7bc48ff471113dad488f5415c141cfb77e004e52f6ab5b7cb3f8fb0b3e/docling_parse-5.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:72762914e05708b67d65b9307be97376a0e6d4cc014f7899887c51d68c722841", size = 11322428, upload-time = "2026-04-24T15:02:15.567Z" }, ] -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - -[[package]] -name = "dotty-dict" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/ab/88d67f02024700b48cd8232579ad1316aa9df2272c63049c27cc094229d6/dotty_dict-1.3.1.tar.gz", hash = "sha256:4b016e03b8ae265539757a53eba24b9bfda506fb94fbce0bee843c6f05541a15", size = 7699, upload-time = "2022-07-09T18:50:57.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/91/e0d457ee03ec33d79ee2cd8d212debb1bc21dfb99728ae35efdb5832dc22/dotty_dict-1.3.1-py3-none-any.whl", hash = "sha256:5022d234d9922f13aa711b4950372a06a6d64cb6d6db9ba43d0ba133ebfce31f", size = 7014, upload-time = "2022-07-09T18:50:55.058Z" }, -] - [[package]] name = "elastic-transport" version = "8.17.1" @@ -1634,30 +1594,6 @@ http = [ { name = "aiohttp", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.48" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/c3/e4a9656f3cdb280f5dc65a68cc6fc46e79f897d27c1a36bbb4f0f47aaac5/gitpython-3.1.48.tar.gz", hash = "sha256:b7c49ff4a49946fce38ac84116efa311b15e7dad06dc3787fc9e206bf9ef75e1", size = 217288, upload-time = "2026-04-28T05:35:45.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/50/bb9703c364c00e7be67ccda03536f3d684766ce109d184c9d1f072d866ca/gitpython-3.1.48-py3-none-any.whl", hash = "sha256:737698b05889cca0f9aba7054d796620df2092c68926ee1470e5c7f5ac886680", size = 209800, upload-time = "2026-04-28T05:35:42.543Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.74.0" @@ -2093,15 +2029,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/a8/7a1659fa61a225ea23b8e5d6d376e3ec427c994116141e6a1617dc253e0a/installer-1.0.0-py3-none-any.whl", hash = "sha256:7b46327ded20d8544bfe2d8561618bbcd12d88e7e3645333af1ed141d8bc1bfe", size = 464228, upload-time = "2026-03-28T15:39:20.345Z" }, ] -[[package]] -name = "invoke" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, -] - [[package]] name = "ipykernel" version = "7.2.0" @@ -2198,42 +2125,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, -] - -[[package]] -name = "jaraco-context" -version = "6.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/50/4763cd07e722bb6285316d390a164bc7e479db9d90daa769f22578f698b4/jaraco_context-6.1.2.tar.gz", hash = "sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3", size = 16801, upload-time = "2026-03-20T22:13:33.922Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/58/bc8954bda5fcda97bd7c19be11b85f91973d67a706ed4a3aec33e7de22db/jaraco_context-6.1.2-py3-none-any.whl", hash = "sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535", size = 7871, upload-time = "2026-03-20T22:13:32.808Z" }, -] - -[[package]] -name = "jaraco-functools" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, -] - [[package]] name = "jedi" version = "0.19.2" @@ -2246,15 +2137,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] -[[package]] -name = "jeepney" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -2703,24 +2585,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] -[[package]] -name = "keyring" -version = "25.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, -] - [[package]] name = "langchain-classic" version = "1.0.4" @@ -3334,7 +3198,7 @@ wheels = [ [[package]] name = "mellea" -version = "0.6.0" +version = "0.7.0.dev0" source = { editable = "." } dependencies = [ { name = "jinja2" }, @@ -3492,7 +3356,6 @@ dev = [ { name = "pytest-recording" }, { name = "pytest-timeout" }, { name = "pytest-xdist" }, - { name = "python-semantic-release" }, { name = "ruff" }, { name = "sentencepiece" }, { name = "types-requests" }, @@ -3509,9 +3372,6 @@ notebook = [ { name = "ipython" }, { name = "jupyter" }, ] -release = [ - { name = "python-semantic-release" }, -] test = [ { name = "ddgs" }, { name = "jsonschema", version = "4.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, @@ -3612,7 +3472,6 @@ dev = [ { name = "pytest-recording" }, { name = "pytest-timeout" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "python-semantic-release", specifier = "~=7.32" }, { name = "ruff", specifier = ">=0.15.5" }, { name = "sentencepiece", specifier = "==0.2.1" }, { name = "types-requests" }, @@ -3629,7 +3488,6 @@ notebook = [ { name = "ipython", specifier = ">=8.36.0" }, { name = "jupyter", specifier = ">=1.1.1" }, ] -release = [{ name = "python-semantic-release", specifier = "~=7.32" }] test = [ { name = "ddgs", specifier = ">=9.0.0" }, { name = "jsonschema" }, @@ -3669,15 +3527,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, ] -[[package]] -name = "more-itertools" -version = "11.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, -] - [[package]] name = "mpire" version = "2.10.2" @@ -4045,40 +3894,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] -[[package]] -name = "nh3" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/5f/1d19bdc7d27238e37f3672cdc02cb77c56a4a86d140cd4f4f23c90df6e16/nh3-0.3.5.tar.gz", hash = "sha256:45855e14ff056064fec77133bfcf7cd691838168e5e17bbef075394954dc9dc8", size = 20743, upload-time = "2026-04-25T10:44:16.066Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/63/b0/8587ac42a9627ab88e7e221601f1dfccbf4db80b2a29222ea63266dc9abc/nh3-0.3.5-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:23a312224875f72cd16bde417f49071451877e29ef646a60e50fcb69407cc18a", size = 1420126, upload-time = "2026-04-25T10:43:39.834Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/1dbc4d0c43f12e8c1784ede17eaee6f061d4fbe5505757c65c49b2ceab95/nh3-0.3.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387abd011e81959d5a35151a11350a0795c6edeb53ebfa02d2e882dc01299263", size = 793943, upload-time = "2026-04-25T10:43:41.363Z" }, - { url = "https://files.pythonhosted.org/packages/47/9f/d6758d7a14ee964bf439cc35ae4fa24a763a93399c8ef6f22bd11d532d29/nh3-0.3.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48f45e3e914be93a596431aa143dedf1582557bf41a58153c296048d6e3798c9", size = 841150, upload-time = "2026-04-25T10:43:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/b6/36/d5d1ae8374612c98f390e1ea7c610fa6c9716259a03bbf4d15b269f40073/nh3-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0a09f51806fd51b4fedbf9ea2b61fef388f19aef0d62fe51199d41648be14588", size = 1008415, upload-time = "2026-04-25T10:43:44.324Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8f/d13a9c3fd2d9c131a2a281737380e9379eb0f8c33fea24c2b923aaafbb15/nh3-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c357f1d042c67f135a5e6babb2b0e3b9d9224ff4a3543240f597767b01384ffd", size = 1092706, upload-time = "2026-04-25T10:43:45.653Z" }, - { url = "https://files.pythonhosted.org/packages/bb/57/2f3add7f8680fcc896afa6a675cb2bab09982853ee8af40bad621f6b61c4/nh3-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:38748140bf76383ab7ce2dce0ad4cb663855d8fbc9098f7f3483673d09616a17", size = 1048346, upload-time = "2026-04-25T10:43:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c3/2f9e4ffa82863074d1361bfe949bc46393d91b3411579dfbbd090b24cac5/nh3-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:84bdeb082544fbcb77a12c034dd77d7da0556fdc0727b787eb6214b958c15e29", size = 1029038, upload-time = "2026-04-25T10:43:48.569Z" }, - { url = "https://files.pythonhosted.org/packages/e8/10/2804deb3f3315184c9cae41702e293c87524b5a21f766b07d7fe3ffbcfbb/nh3-0.3.5-cp314-cp314t-win32.whl", hash = "sha256:c3aae321f67ae66cff2a627115f106a377d4475d10b0e13d97959a13486b9a88", size = 603263, upload-time = "2026-04-25T10:43:49.851Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/f6685248b49f7548fc9a8c335ab3a52f68610b72e8a61576447151e4e2e6/nh3-0.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c88605d8d468f7fc1b31e06129bc91d6c96f6c621776c9b504a0da9beac9df5f", size = 616866, upload-time = "2026-04-25T10:43:51.005Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/d8c9018635d4acfefde6b68470daa510eed715a350cbaa2f928ba0609f81/nh3-0.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:72c5bdedec27fa33de6a5326346ea8aa3fe54f6ac294d54c4b204fb66a9f1e79", size = 602566, upload-time = "2026-04-25T10:43:52.283Z" }, - { url = "https://files.pythonhosted.org/packages/85/30/d162e99746a2fb1d98bb0ef23af3e201b156cf09f7de867c7390c8fe1c06/nh3-0.3.5-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3bb854485c9b33e5bb143ff3e49e577073bc6bc320f0ff8fc316dd89c0d3c101", size = 1442393, upload-time = "2026-04-25T10:43:53.556Z" }, - { url = "https://files.pythonhosted.org/packages/25/8c/072120d506978ab053e1732d0efa7c86cb478fee0ee098fda0ac0d31cb34/nh3-0.3.5-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50d401ab2d8e86d59e2126e3ab2a2f45840c405842b626d9a51624b3a33b6878", size = 837722, upload-time = "2026-04-25T10:43:55.073Z" }, - { url = "https://files.pythonhosted.org/packages/52/86/d4e06e28c5ad1c4b065f89737d02631bd49f1660b6ebcf17a87ffcd201da/nh3-0.3.5-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acfd354e61accbe4c74f8017c6e397a776916dfe47c48643cf7fd84ade826f93", size = 822872, upload-time = "2026-04-25T10:43:56.581Z" }, - { url = "https://files.pythonhosted.org/packages/0a/62/50659255213f241ec5797ae7427464c969397373e83b3659372b341ae869/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:52d877980d7ca01dc3baf3936bf844828bc6f332962227a684ed79c18cce14c3", size = 1100031, upload-time = "2026-04-25T10:43:58.098Z" }, - { url = "https://files.pythonhosted.org/packages/00/7a/a12ae77593b2fcf3be25df7bc1c01967d0de448bdb4b6c7ec80fe4f5a74f/nh3-0.3.5-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:207c01801d3e9bb8ec08f08689346bdd30ce15b8bf60013a925d08b5388962a4", size = 1057669, upload-time = "2026-04-25T10:43:59.328Z" }, - { url = "https://files.pythonhosted.org/packages/2d/71/5647dc04c0233192a3956fc91708822b21403a06508cacf78083c68e7bf0/nh3-0.3.5-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea232933394d1d58bf7c4bb348dc4660eae6604e1ae81cd2ba6d9ed80d390f3b", size = 914795, upload-time = "2026-04-25T10:44:00.52Z" }, - { url = "https://files.pythonhosted.org/packages/1b/0e/bf298920729f216adcb002acf7ea01b90842603d2e4e2ce9b900d9ee8fab/nh3-0.3.5-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe3a787dc76b50de6bee54ef242f26c41dfe47654428e3e94f0fae5bb6dd2cc1", size = 806976, upload-time = "2026-04-25T10:44:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/85/01/26761e1dc2b848e65a62c19e5d39ad446283287cd4afddc89f364ab86bc9/nh3-0.3.5-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:488928988caad25ba14b1eb5bc74e25e21f3b5e40341d956f3ce4a8bc19460dc", size = 834904, upload-time = "2026-04-25T10:44:03.454Z" }, - { url = "https://files.pythonhosted.org/packages/33/53/0766113e679540ac1edc1b82b1295aecd321eeb75d6fead70109a838b6ee/nh3-0.3.5-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c069570b06aa848457713ad7af4a9905691291548c4466a9ad78ee95808382b", size = 857159, upload-time = "2026-04-25T10:44:05.003Z" }, - { url = "https://files.pythonhosted.org/packages/58/36/734d353dfaf292fed574b8b3092f0ef79dc6404f3879f7faaa61a4701fad/nh3-0.3.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eeedc90ed8c42c327e8e10e621ccfa314fc6cce35d5929f4297ff1cdb89667c4", size = 1018600, upload-time = "2026-04-25T10:44:06.18Z" }, - { url = "https://files.pythonhosted.org/packages/6b/aa/d9c59c1b49669fcb7bababa55df82385f029ad5c2651f583c3a1141cfdd1/nh3-0.3.5-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:de8e8621853b6470fe928c684ee0d3f39ea8086cebafe4c416486488dea7b68d", size = 1103530, upload-time = "2026-04-25T10:44:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/90/b0/cdd210bfb8d9d43fb02fc3c868336b9955934d8e15e66eb1d15a147b8af0/nh3-0.3.5-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:6ea58cc44d274c643b83547ca9654a0b1a817609b160601356f76a2b744c49ad", size = 1061754, upload-time = "2026-04-25T10:44:09.362Z" }, - { url = "https://files.pythonhosted.org/packages/ce/cb/7a39e72e668c8445bdd95e494b3e21cfdddc68329be8ea3522c8befb46c4/nh3-0.3.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e49c9b564e6bcb03ecd2f057213df9a0de15a95812ac9db9600b590db23d3ae9", size = 1040938, upload-time = "2026-04-25T10:44:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/af/4c/fc2f9ed208a3801a319f59b5fea03cdc20cf3bd8af14be930d3a8de01224/nh3-0.3.5-cp38-abi3-win32.whl", hash = "sha256:559e4c73b689e9a7aa97ac9760b1bc488038d7c1a575aa4ab5a0e19ee9630c0f", size = 611445, upload-time = "2026-04-25T10:44:12.317Z" }, - { url = "https://files.pythonhosted.org/packages/db/1a/e4c9b5e2ae13e6092c9ec16d8ca30646cb01fcdea245f36c5b08fd21fbd5/nh3-0.3.5-cp38-abi3-win_amd64.whl", hash = "sha256:45e6a65dc88a300a2e3502cb9c8e6d1d6b831d6fba7470643333609c6aab1f30", size = 626502, upload-time = "2026-04-25T10:44:13.682Z" }, - { url = "https://files.pythonhosted.org/packages/80/7c/19cd0671d1ba2762fb388fc149697d20d0568ccfeef833b11280a619e526/nh3-0.3.5-cp38-abi3-win_arm64.whl", hash = "sha256:8f85285700a18e9f3fc5bff41fe573fa84f81542ef13b48a89f9fecca0474d3b", size = 611069, upload-time = "2026-04-25T10:44:14.934Z" }, -] - [[package]] name = "nltk" version = "3.9.4" @@ -4992,15 +4807,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, ] -[[package]] -name = "pkginfo" -version = "1.12.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/03/e26bf3d6453b7fda5bd2b84029a426553bb373d6277ef6b5ac8863421f87/pkginfo-1.12.1.2.tar.gz", hash = "sha256:5cd957824ac36f140260964eba3c6be6442a8359b8c48f4adf90210f33a04b7b", size = 451828, upload-time = "2025-02-19T15:27:37.188Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/3d/f4f2ba829efb54b6cd2d91349c7463316a9cc55a43fc980447416c88540f/pkginfo-1.12.1.2-py3-none-any.whl", hash = "sha256:c783ac885519cab2c34927ccfa6bf64b5a704d7c69afaea583dd9b7afe969343", size = 32717, upload-time = "2025-02-19T15:27:33.071Z" }, -] - [[package]] name = "platformdirs" version = "4.9.6" @@ -5976,19 +5782,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] -[[package]] -name = "python-gitlab" -version = "3.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "requests-toolbelt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/53/248b87282df591d74ba3d38c3c3ced2b5087248c0ccfb6b3a947bb1034c3/python-gitlab-3.15.0.tar.gz", hash = "sha256:c9e65eb7612a9fbb8abf0339972eca7fd7a73d4da66c9b446ffe528930aff534", size = 273270, upload-time = "2023-06-09T09:51:31.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/51/3c7dd08272658e5490d0c0b6c94af15bd0c0649e7ad23c9ed0db1d276143/python_gitlab-3.15.0-py3-none-any.whl", hash = "sha256:8f8d1c0d387f642eb1ac7bf5e8e0cd8b3dd49c6f34170cee3c7deb7d384611f3", size = 135865, upload-time = "2023-06-09T09:51:29.996Z" }, -] - [[package]] name = "python-json-logger" version = "4.1.0" @@ -6022,30 +5815,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" }, ] -[[package]] -name = "python-semantic-release" -version = "7.34.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, - { name = "click", version = "8.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, - { name = "click-log" }, - { name = "dotty-dict" }, - { name = "gitpython" }, - { name = "invoke" }, - { name = "packaging" }, - { name = "python-gitlab" }, - { name = "requests" }, - { name = "semver" }, - { name = "tomlkit" }, - { name = "twine" }, - { name = "wheel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/67/abf0ed527dafc5545b2ae264ec090b9849fac7775c319f0f6da95a50e9b7/python-semantic-release-7.34.6.tar.gz", hash = "sha256:e9b8fb788024ae9510a924136d573588415a16eeca31cc5240f2754a80a2e831", size = 41885, upload-time = "2023-06-17T14:12:17.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/53/d9b4c4a811a946489f89b62b02b01e9e456dc8c3bde154a18eac4f1dcbe4/python_semantic_release-7.34.6-py3-none-any.whl", hash = "sha256:7e3969ba4663d9b2087b02bf3ac140e202551377bf045c34e09bfe19753e19ab", size = 55637, upload-time = "2023-06-17T14:12:14.975Z" }, -] - [[package]] name = "pytz" version = "2026.1.post1" @@ -6074,15 +5843,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, -] - [[package]] name = "pywinpty" version = "3.0.3" @@ -6237,20 +5997,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/4a/fa521d947f0fc7bb304bf11bec4cb66266bd81494588b4cb48dc01001719/rapidocr-3.8.1-py3-none-any.whl", hash = "sha256:650044b1fbce9e6bae5cae462dcf8be754cde11e2f23fc51f65dcc08deae2c46", size = 15080319, upload-time = "2026-04-11T07:13:22.56Z" }, ] -[[package]] -name = "readme-renderer" -version = "44.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "nh3" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, -] - [[package]] name = "referencing" version = "0.37.0" @@ -6417,15 +6163,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, ] -[[package]] -name = "rfc3986" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, -] - [[package]] name = "rfc3986-validator" version = "0.1.1" @@ -6783,19 +6520,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, ] -[[package]] -name = "secretstorage" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, -] - [[package]] name = "semchunk" version = "3.2.5" @@ -6809,15 +6533,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/95/12d226ee4d207cb1f77a216baa7e1a8bae2639733c140abe8d0316d23a18/semchunk-3.2.5-py3-none-any.whl", hash = "sha256:fd09cc5f380bd010b8ca773bd81893f7eaf11d37dd8362a83d46cedaf5dae076", size = 13048, upload-time = "2025-10-28T02:12:36.724Z" }, ] -[[package]] -name = "semver" -version = "2.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/a9/b61190916030ee9af83de342e101f192bbb436c59be20a4cb0cdb7256ece/semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f", size = 45816, upload-time = "2020-10-20T20:16:54.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/70/b84f9944a03964a88031ef6ac219b6c91e8ba2f373362329d8770ef36f02/semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4", size = 12901, upload-time = "2020-10-20T20:16:52.583Z" }, -] - [[package]] name = "send2trash" version = "2.1.0" @@ -6988,15 +6703,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "smolagents" version = "1.24.0" @@ -7625,28 +7331,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] -[[package]] -name = "twine" -version = "3.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, - { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" }, - { name = "keyring" }, - { name = "pkginfo" }, - { name = "readme-renderer" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "rfc3986" }, - { name = "tqdm" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/3e/ce331d7e215abdc16c53e65f8506bfccf4840ce191b709a37b8c83cc32c7/twine-3.8.0.tar.gz", hash = "sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19", size = 214568, upload-time = "2022-02-02T18:50:23.428Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/74/ea7dfb86223695fd8efa256a24d1520729dde79a4e628ee6879f0f136d40/twine-3.8.0-py3-none-any.whl", hash = "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8", size = 36057, upload-time = "2022-02-02T18:50:21.723Z" }, -] - [[package]] name = "typer" version = "0.21.2" @@ -7927,18 +7611,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] -[[package]] -name = "wheel" -version = "0.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/62/75f18a0f03b4219c456652c7780e4d749b929eb605c098ce3a5b6b6bc081/wheel-0.47.0.tar.gz", hash = "sha256:cc72bd1009ba0cf63922e28f94d9d83b920aa2bb28f798a31d0691b02fa3c9b3", size = 63854, upload-time = "2026-04-22T15:51:27.727Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/1b/9e33c09813d65e248f7f773119148a612516a4bea93e9c6f545f78455b7c/wheel-0.47.0-py3-none-any.whl", hash = "sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced", size = 32218, upload-time = "2026-04-22T15:51:26.296Z" }, -] - [[package]] name = "widgetsnbextension" version = "4.0.15"