From c7fa0e7b79c51837d57b1643435efb061d01b116 Mon Sep 17 00:00:00 2001 From: Brady Holt Date: Wed, 19 Aug 2026 17:36:47 -0500 Subject: [PATCH 1/2] Set the release version in the repo instead of in CI The publish workflow bumped the version and pushed the commit to main, which the tier-1-repos ruleset blocks without a PAT. It also ran generate.sh to embed the new version in the client, but openapi-generator is not installed on the runner and generate.sh had no set -e, so that step silently did nothing. 4.3.0 and 4.4.0 shipped with __version__ = "4.2.0" and a matching stale user-agent. The version now comes from pyproject.toml on main, set by the same PR that regenerates the client, where the generator is actually installed. generate.sh takes an optional major/minor/patch argument (defaulting to minor) and bumps before generating. Publish only tests, builds, publishes, and releases the commit it checked out, all of which GITHUB_TOKEN can do, so GH_API_TOKEN can be deleted from repo secrets. --- .github/workflows/publish.yml | 47 +++++++++++++---------------------- DEVELOPMENT.md | 11 +++++--- scripts/generate-and-pr.sh | 34 +++++++++++++++++++------ scripts/generate.sh | 32 +++++++++++++++++++++--- 4 files changed, 80 insertions(+), 44 deletions(-) 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..09ea48c 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -9,10 +9,15 @@ ## 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. +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. -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. +Alternatively, run `scripts/generate-and-pr.sh` to do the above and automate the PR. It takes the same optional version type argument. It wraps `generate.sh`, detects the old → new spec version, and (after you confirm) bumps the package version and 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. ## 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 index e976b62..4981acb 100755 --- a/scripts/generate-and-pr.sh +++ b/scripts/generate-and-pr.sh @@ -2,14 +2,25 @@ # # Regenerate the SDK from the latest YNAB API spec, then optionally open a PR. # +# Usage: scripts/generate-and-pr.sh [major|minor|patch] (defaults to minor) +# # 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. +# version and, if you confirm, bumps the package version, 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 +VERSION_TYPE="${1:-minor}" +case "$VERSION_TYPE" in + major | minor | patch) ;; + *) + echo "Invalid version type: $VERSION_TYPE (expected major, minor, or patch)" >&2 + exit 1 + ;; +esac + REPO_ROOT="$(git rev-parse --show-toplevel)" SCRIPT_DIR="$REPO_ROOT/scripts" SPEC="$REPO_ROOT/open_api_spec.yaml" @@ -24,8 +35,9 @@ spec_version() { 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" +# Regenerate without bumping the version, so an unchanged spec leaves the working +# tree clean and the no-op check below still holds. +bash "$SCRIPT_DIR/generate.sh" none NEW_VERSION="$(spec_version < "$SPEC")" @@ -49,6 +61,14 @@ case "$reply" in ;; esac +# The published version comes from pyproject.toml on main, so the release bump +# rides along in this PR. The generator embeds it, hence the second pass. +echo +echo "Bumping package version (${VERSION_TYPE}) and regenerating..." +PACKAGE_VERSION="$(python3 "$SCRIPT_DIR/update_version.py" "$VERSION_TYPE")" +bash "$SCRIPT_DIR/generate.sh" none +echo "Package version: ${PACKAGE_VERSION}" + # 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. @@ -75,7 +95,7 @@ esac 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})." + -m "Regenerated the client from the YNAB API spec ${NEW_VERSION} (previously ${OLD_VERSION:-unknown}) and set the package version to ${PACKAGE_VERSION}." git push -u origin "$BRANCH" # Use claude only to draft the PR description from the meaningful diff (the spec 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!" From 144c73eaf0b740099d04f6023692dd3f914f5c59 Mon Sep 17 00:00:00 2001 From: Brady Holt Date: Wed, 19 Aug 2026 17:44:26 -0500 Subject: [PATCH 2/2] Remove scripts/generate-and-pr.sh The script wrapped generate.sh with branch, commit, push, and PR creation, but never used generate.sh's version bump: it called it with "none" and bumped separately, leaving two entry points with different semantics. Regenerating and opening the PR is straightforward enough by hand that the wrapper was not worth maintaining alongside it. --- DEVELOPMENT.md | 2 - scripts/generate-and-pr.sh | 131 ------------------------------------- 2 files changed, 133 deletions(-) delete mode 100755 scripts/generate-and-pr.sh diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 09ea48c..0a12352 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -11,8 +11,6 @@ 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. -Alternatively, run `scripts/generate-and-pr.sh` to do the above and automate the PR. It takes the same optional version type argument. It wraps `generate.sh`, detects the old → new spec version, and (after you confirm) bumps the package version and 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. - ## Publishing The version that gets published is whatever `pyproject.toml` on the `main` branch says, so publishing is two steps. diff --git a/scripts/generate-and-pr.sh b/scripts/generate-and-pr.sh deleted file mode 100755 index 4981acb..0000000 --- a/scripts/generate-and-pr.sh +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env bash -# -# Regenerate the SDK from the latest YNAB API spec, then optionally open a PR. -# -# Usage: scripts/generate-and-pr.sh [major|minor|patch] (defaults to minor) -# -# Wraps scripts/generate.sh. After regenerating it detects the old -> new spec -# version and, if you confirm, bumps the package version, 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 - -VERSION_TYPE="${1:-minor}" -case "$VERSION_TYPE" in - major | minor | patch) ;; - *) - echo "Invalid version type: $VERSION_TYPE (expected major, minor, or patch)" >&2 - exit 1 - ;; -esac - -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 without bumping the version, so an unchanged spec leaves the working -# tree clean and the no-op check below still holds. -bash "$SCRIPT_DIR/generate.sh" none - -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 - -# The published version comes from pyproject.toml on main, so the release bump -# rides along in this PR. The generator embeds it, hence the second pass. -echo -echo "Bumping package version (${VERSION_TYPE}) and regenerating..." -PACKAGE_VERSION="$(python3 "$SCRIPT_DIR/update_version.py" "$VERSION_TYPE")" -bash "$SCRIPT_DIR/generate.sh" none -echo "Package version: ${PACKAGE_VERSION}" - -# 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}) and set the package version to ${PACKAGE_VERSION}." -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."