Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Auto-tag SDK release

on:
push:
branches: [main]

concurrency:
group: auto-tag-sdk-release
cancel-in-progress: false

permissions:
contents: read

jobs:
tag-and-dispatch:
runs-on: ubuntu-latest
permissions:
actions: write
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Find a release version
id: version
shell: bash
run: |
python scripts/check_version.py
version="$(python -c 'import runpy; print(runpy.run_path("src/kanopy/_version.py")["__version__"])')"
tag="v${version}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
if git show-ref --verify --quiet "refs/tags/${tag}"; then
if ! git diff --quiet "${tag}..HEAD" -- \
src setup.cfg pyproject.toml MANIFEST.in README.md LICENSE NOTICE; then
echo "::error::Release-relevant files changed, but ${tag} already exists. Bump src/kanopy/_version.py."
exit 1
fi
echo "No release-relevant files changed since ${tag}; nothing to publish."
echo "publish=false" >> "${GITHUB_OUTPUT}"
else
echo "Version ${version} will be released as ${tag}."
echo "publish=true" >> "${GITHUB_OUTPUT}"
fi
- name: Create release tag
if: steps.version.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: |
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
-f ref="refs/tags/${RELEASE_TAG}" \
-f sha="${GITHUB_SHA}"
- name: Dispatch trusted release workflow
if: steps.version.outputs.publish == 'true'
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: gh workflow run release.yml --repo "${GITHUB_REPOSITORY}" --ref "${RELEASE_TAG}"
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Release to PyPI
on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: read
Expand Down
14 changes: 10 additions & 4 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,24 @@ token belongs in GitHub, a local environment, or this repository.

## Release checklist

1. Choose the version and update both `setup.cfg` and
`src/kanopy/__init__.py`.
1. Choose the version and update `src/kanopy/_version.py`.
2. From the private Kanopy development repository, run
`./scripts/sync_public_openapi.sh`, then review and commit the SDK fixture.
3. Run `./scripts/run_local_smoke.sh` against the current backend Docker build.
4. Run `python -m pytest`, Ruff checks, and a clean package build.
5. Merge through the protected `main` branch and confirm CI passes.
6. Create and push the matching tag, for example `v0.1.0`.
7. Review and approve the `pypi` deployment environment.
6. Confirm that `Auto-tag SDK release` created the matching tag and dispatched
`Release to PyPI`. The workflow fails instead of silently skipping when
release-relevant files changed without a version bump.
7. Review and approve the `pypi` deployment environment. This is the only
manual publishing step.
8. Install the exact published version into a clean environment and run the
read-only identity/project-list smoke check against staging.

If automatic dispatch fails after the tag is created, run `Release to PyPI`
manually against that tag from the GitHub Actions page. A direct `v*` tag push
also remains supported.

The release job rebuilds nothing after approval: the publishing job downloads
the exact wheel and source distribution produced and inspected by the build
job. Trusted Publishing also creates PyPI attestations by default.
2 changes: 1 addition & 1 deletion src/kanopy/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
without importing the package __init__ (which imports client.py back).
"""

__version__ = "0.6.0"
__version__ = "0.7.0"
22 changes: 22 additions & 0 deletions src/kanopy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"update_job": ("patch", "/jobs/{job_id}", "update_job"),
"delete_job": ("delete", "/jobs/{job_id}", "delete_job"),
"cancel_job": ("post", "/jobs/{job_id}/cancel", "cancel_job"),
"upload_heartbeat": ("post", "/jobs/upload-heartbeat", "upload_heartbeat"),
"upload": ("post", "/upload", "upload"),
"init_presigned_multipart_upload": (
"post",
Expand Down Expand Up @@ -405,6 +406,27 @@ def delete_job(self, job_id: str) -> None:
def cancel_job(self, job_id: str) -> JsonObject:
return self._object(self._json("POST", f"/jobs/{job_id}/cancel"))

def upload_heartbeat(self, job_ids: Sequence[str]) -> JsonObject:
"""Renew the hold on jobs that are reserved but not yet uploading.

A job created ahead of its upload is held for 30 minutes; if no video
data has arrived by then it is treated as abandoned and deleted. A
script that reserves jobs for a batch up front and uploads them one at
a time will outlive that hold, and the jobs still waiting are deleted
mid-run. Call this every few minutes with the ids still waiting.

Jobs whose upload has already begun need no heartbeat. The returned
``missing`` ids no longer exist and cannot be revived — re-create the
job to retry that video.
"""
return self._object(
self._json(
"POST",
"/jobs/upload-heartbeat",
json={"job_ids": [str(job_id) for job_id in job_ids]},
)
)

def wait_for_job(
self,
job_id: str,
Expand Down
Loading
Loading