diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7cfc205..13ff15b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,60 +1,47 @@ name: Publish on: workflow_dispatch: - inputs: - version_type: - type: choice - description: Version type - default: minor - options: - - major - - minor - - patch jobs: publish: runs-on: ubuntu-latest permissions: id-token: write # required for PyPI Trusted Publishing (OIDC) - contents: write # create the GitHub Release + contents: write # create the tag and GitHub Release pull-requests: write # comment on PRs included in the release steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - token: ${{ secrets.GH_API_TOKEN }} - - name: Setup git repo - run: | - git config user.name $GITHUB_ACTOR - git config user.email gh-actions-${GITHUB_ACTOR}@github.com - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: 3.12 + - name: Resolve version to publish + id: version + run: | + version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["tool"]["poetry"]["version"])') + echo "version=${version}" >> $GITHUB_OUTPUT + - name: Verify the release tag does not already exist + env: + TAG_NAME: ${{ steps.version.outputs.version }} + run: | + if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" >/dev/null 2>&1; then + echo "::error::Tag ${TAG_NAME} already exists. Bump the version in pyproject.toml first." + exit 1 + fi - name: Install dependencies run: | pip install poetry poetry install - - name: Bump Version - id: bump_version - run: | - NEW_VERSION=$(python scripts/update_version.py ${{ github.event.inputs.version_type }}) - echo "NEW_VERSION=$NEW_VERSION" - echo "new_version=$(echo $NEW_VERSION)" >> $GITHUB_OUTPUT - - name: Generate with new version - run: ./scripts/generate.sh + - name: Test with pytest + run: poetry run pytest - name: Build run: poetry build - - name: Commit and push changes - run: | - git add ynab/configuration.py ynab/api_client.py ynab/__init__.py pyproject.toml openapi-generator-config.yaml - git commit -m 'Bumping version for ${{ steps.bump_version.outputs.new_version }}' - git push - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 - name: Create a Release id: create-release uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 with: - tag_name: ${{ steps.bump_version.outputs.new_version }} + tag_name: ${{ steps.version.outputs.version }} generate_release_notes: true - name: Comment on PRs with link to release they are included in uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 70c2ad9..0a12352 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,10 +9,13 @@ ## Generating -1. Run `scripts/generate.sh`. This will generate the API client from the latest OpenAPI spec. Once generated, you should open a PR and merge the changes. - -Alternatively, run `scripts/generate-and-pr.sh` to do the above and automate the PR. It wraps `generate.sh`, detects the old → new spec version, and (after you confirm) ensures you're on a `gen-` branch, then commits, pushes, and opens a PR against `main` via `git`/`gh`, printing the PR URL. The [`claude`](https://docs.claude.com/en/docs/claude-code/overview) CLI is used only to draft the PR description from the spec diff. Requires the `gh` and `claude` CLIs to be installed and authenticated. +1. Run `scripts/generate.sh`. This will bump the package version and generate the API client from the latest OpenAPI spec. The bump defaults to `minor`; pass `major` or `patch` to change it (`scripts/generate.sh patch`), or `none` to regenerate without bumping. Once generated, you should open a PR and merge the changes. ## Publishing -Run the "Publish" GitHub Actions workflow. \ No newline at end of file +The version that gets published is whatever `pyproject.toml` on the `main` branch says, so publishing is two steps. + +1. Merge a PR that sets the new version. `scripts/generate.sh` does this for you. For a release that does not involve re-generating the client, run `python3 scripts/update_version.py minor` (or `major` / `patch`), which updates `pyproject.toml` and `openapi-generator-config.yaml` without creating a commit or tag. +2. Run the "Publish" GitHub Actions workflow. This builds, tests, and publishes that version to PyPI, then tags the commit and creates a GitHub release. + +The workflow never changes the version itself. If the version in `pyproject.toml` has already been released, the workflow fails before publishing anything. \ No newline at end of file diff --git a/scripts/generate-and-pr.sh b/scripts/generate-and-pr.sh deleted file mode 100755 index e976b62..0000000 --- a/scripts/generate-and-pr.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# -# Regenerate the SDK from the latest YNAB API spec, then optionally open a PR. -# -# Wraps scripts/generate.sh. After regenerating it detects the old -> new spec -# version and, if you confirm, ensures you're on a gen- branch, then -# commits, pushes, and opens a PR via git/gh. The `claude` CLI is used only to -# draft the PR description from the spec diff. Requires the `gh` and `claude` -# CLIs to be installed and authenticated. - -set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel)" -SCRIPT_DIR="$REPO_ROOT/scripts" -SPEC="$REPO_ROOT/open_api_spec.yaml" - -cd "$REPO_ROOT" - -# Extract info.version from an OpenAPI spec on stdin (the 2-space-indented -# `version:` inside the top-level `info:` block, not `openapi:` or nested ones). -spec_version() { - awk '/^info:/{f=1} f && /^ version:/{print $2; exit}' -} - -OLD_VERSION="$(git show HEAD:open_api_spec.yaml 2>/dev/null | spec_version || true)" - -# Regenerate: downloads the latest spec and runs openapi-generator. -bash "$SCRIPT_DIR/generate.sh" - -NEW_VERSION="$(spec_version < "$SPEC")" - -if [[ -z "$(git status --porcelain)" ]]; then - echo - echo "No changes after regeneration — spec ${NEW_VERSION:-unknown} is already up to date. Nothing to do." - exit 0 -fi - -echo -echo "Regeneration produced changes. Spec version: ${OLD_VERSION:-unknown} -> ${NEW_VERSION:-unknown}" -git status --short -echo - -read -r -p "Create a pull request for these changes? [y/N] " reply -case "$reply" in - [yY] | [yY][eE][sS]) ;; - *) - echo "Leaving the regenerated changes uncommitted in the working tree. Done." - exit 0 - ;; -esac - -# Never commit on a protected branch: if we're on one, create a fresh -# gen- branch (the uncommitted regen changes carry over). Otherwise -# stay on the current feature branch. -BRANCH="$(git rev-parse --abbrev-ref HEAD)" -DESIRED="gen-${NEW_VERSION//./-}" -case "$BRANCH" in - main | master | develop) - target="$DESIRED" - if git show-ref --verify --quiet "refs/heads/$target"; then - n=2 - while git show-ref --verify --quiet "refs/heads/${DESIRED}-${n}"; do n=$((n + 1)); done - target="${DESIRED}-${n}" - fi - echo "On protected branch '$BRANCH'; creating and switching to '$target'." - git switch -c "$target" - BRANCH="$target" - ;; - *) - echo "Committing on current branch: '$BRANCH'." - ;; -esac - -# Commit and push the regenerated client. -git add -A -git commit \ - -m "Regenerate SDK from server specification version ${NEW_VERSION}" \ - -m "Regenerated the client from the YNAB API spec ${NEW_VERSION} (previously ${OLD_VERSION:-unknown})." -git push -u origin "$BRANCH" - -# Use claude only to draft the PR description from the meaningful diff (the spec -# and docs; the per-file churn under ynab/ is noise). Everything else is gh/git. -echo -echo "Drafting the PR description with Claude..." -DIFF="$(git diff "origin/main...HEAD" -- open_api_spec.yaml docs/; echo; git diff --stat "origin/main...HEAD")" - -BODY_FILE="$(mktemp)" -trap 'rm -f "$BODY_FILE"' EXIT - -claude -p "Write a GitHub pull request description for a regeneration of the YNAB Python SDK from the YNAB OpenAPI spec (version ${OLD_VERSION:-unknown} -> ${NEW_VERSION}). - -Summarize the FUNCTIONAL API changes (new or changed fields, endpoints, enums, response codes) from the diff below, focusing on open_api_spec.yaml and docs/. If there are no functional changes, say it is a routine spec-generation refresh. Ignore the mechanical per-file header/version churn under ynab/. - -Output ONLY the PR description as Markdown — no preamble and no surrounding code fence. Do not use any tools. - -Diff: -${DIFF}" > "$BODY_FILE" - -if [[ ! -s "$BODY_FILE" ]]; then - echo "Regenerated the client from the YNAB API spec ${NEW_VERSION} (previously ${OLD_VERSION:-unknown})." > "$BODY_FILE" -fi - -# Open the PR (no reviewers). gh prints the PR URL. -gh pr create \ - --base main \ - --head "$BRANCH" \ - --title "Regenerate SDK from server specification version ${NEW_VERSION}" \ - --body-file "$BODY_FILE" - -echo -echo "Done." diff --git a/scripts/generate.sh b/scripts/generate.sh index 736930b..07f2953 100755 --- a/scripts/generate.sh +++ b/scripts/generate.sh @@ -1,11 +1,35 @@ #!/usr/bin/env bash +# +# Regenerate the API client from the latest YNAB OpenAPI spec and bump the +# package version. +# +# Usage: scripts/generate.sh [major|minor|patch|none] (defaults to minor) +# +# The generator embeds the version in pyproject.toml and the client itself, so +# the bump has to happen before generating. Pass "none" to regenerate without +# bumping. -set -x +set -euo pipefail -echo "Downloading latest YNAB API OpenAPI spec..."; +VERSION_TYPE="${1:-minor}" +case "$VERSION_TYPE" in + major | minor | patch | none) ;; + *) + echo "Invalid version type: $VERSION_TYPE (expected major, minor, patch, or none)" >&2 + exit 1 + ;; +esac + +if [[ "$VERSION_TYPE" != "none" ]]; then + echo "Bumping package version ($VERSION_TYPE)..." + NEW_VERSION="$(python3 scripts/update_version.py "$VERSION_TYPE")" + echo "New package version: $NEW_VERSION" +fi + +echo "Downloading latest YNAB API OpenAPI spec..." wget https://api.ynab.com/papi/open_api_spec.yaml -O ./open_api_spec.yaml echo "Running openapi-generator generate..." -openapi-generator generate -i ./open_api_spec.yaml -g python -t ./templates -c openapi-generator-config.yaml -o ./ +openapi-generator generate -i ./open_api_spec.yaml -g python -t ./templates -c openapi-generator-config.yaml -o ./ -echo "Success!" \ No newline at end of file +echo "Success!"