feat(ci): add PyPI release workflow for splunk-ao-a2a#56
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
Reject bump keywords (patch, major, etc.) and malformed inputs with a clear error before any file is modified, matching the pattern added to the splunk-ao and splunk-ao-adk release workflows Co-authored-by: Cursor <cursoragent@cursor.com>
|
Applied the same changesets from PR #48 to this workflow: Version input validation — Added an upfront |
- Fix regex to correctly accept rc, .dev, and .post PEP 440 forms - Add permissions: contents: read to the build job Co-authored-by: Cursor <cursoragent@cursor.com>
fercor-cisco
left a comment
There was a problem hiding this comment.
🤖 This review was generated by the Astra agent (claude-opus-4-8). It may contain mistakes.
Verdict: approve — Workflow is correct and well-structured (SHA-pinned, least-privilege, OIDC publish); only hardening/design improvements remain, none blocking.
General Comments
- 🟡 minor (design): This release path manually rewrites the version in
pyproject.toml/_version.pyat build time and does not commit or tag the result back to git ("no commit back" per the PR description). Meanwhilesplunk-ao-a2a/pyproject.tomlcarries a full[tool.semantic_release]config (tag_format = "splunk-ao-a2a-v{version}", version_variables/version_toml) that implies releases are meant to be tagged. The result is that a published PyPI version has no corresponding git tag/commit, so there's no traceability from a release artifact back to source. Worth clarifying the intended release model: is semantic-release being abandoned for this package, or should this workflow also create a tag/release? Not blocking, but the two mechanisms are inconsistent.
Follow-ups
Suggested follow-up work that could be tracked as Shortcut stories:
.github/workflows/release-splunk-ao-a2a.yaml:17-17: Consider adding aconcurrencygroup to the workflow (the CI workflow already has one) so two manual dispatches can't race to build/publish the same package simultaneously..github/workflows/release-splunk-ao-a2a.yaml:81-85: There is no guard against re-publishing an already-released version; PyPI will reject a duplicate and the run will fail late. Optionally add an early check (e.g. query the PyPI JSON API for the target version) to fail fast with a clear message before building.
| if ! printf '%s' "$INPUT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$'; then | ||
| echo "::error::Invalid version '${INPUT_VERSION}'. Expected an explicit version like 0.1.0, 1.2.3rc1, or 1.2.3.post1. Bump keywords like 'patch' or 'major' are not accepted." | ||
| exit 1 | ||
| fi | ||
| sed -i "s/^version = \".*\"/version = \"${INPUT_VERSION}\"/" pyproject.toml | ||
| sed -i "s/__version__ = \".*\"/__version__ = \"${INPUT_VERSION}\"/" src/splunk_ao_a2a/_version.py | ||
|
|
||
| - name: Get current version | ||
| id: get-version | ||
| run: | | ||
| VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') | ||
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
🟠 major (security): The version validation can be bypassed by a multi-line input, which then flows unsanitized into the sed replacement string. grep -Eq '^...$' matches on a per-line basis, so an input such as 0.1.0\n<arbitrary> passes the check (line 1 matches) even though the whole value is not a valid version. The raw $INPUT_VERSION is then interpolated into sed -i "s/^version = \".*\"/version = \"${INPUT_VERSION}\"/"; an embedded newline terminates the s command and the remainder is parsed as additional sed commands (e.g. e/w), giving command/file-write execution in CI. workflow_dispatch string inputs are single-line in the UI but can contain newlines when dispatched via the REST API / gh workflow run. Although dispatching requires write access, code execution inside a release pipeline is worth closing off. Prefer bash's [[ =~ ]], whose ^/$ anchor the entire string and where . does not cross newlines, so multi-line input is rejected. The same hardening should be mirrored in any sibling release workflow.
| if ! printf '%s' "$INPUT_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$'; then | |
| echo "::error::Invalid version '${INPUT_VERSION}'. Expected an explicit version like 0.1.0, 1.2.3rc1, or 1.2.3.post1. Bump keywords like 'patch' or 'major' are not accepted." | |
| exit 1 | |
| fi | |
| sed -i "s/^version = \".*\"/version = \"${INPUT_VERSION}\"/" pyproject.toml | |
| sed -i "s/__version__ = \".*\"/__version__ = \"${INPUT_VERSION}\"/" src/splunk_ao_a2a/_version.py | |
| - name: Get current version | |
| id: get-version | |
| run: | | |
| VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| - name: Set version | |
| env: | |
| INPUT_VERSION: ${{ inputs.version }} | |
| run: | | |
| if [[ ! "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?(\.post[0-9]+)?(\.dev[0-9]+)?$ ]]; then | |
| echo "::error::Invalid version '${INPUT_VERSION}'. Expected an explicit version like 0.1.0, 1.2.3rc1, or 1.2.3.post1. Bump keywords like 'patch' or 'major' are not accepted." | |
| exit 1 | |
| fi | |
| sed -i "s/^version = \".*\"/version = \"${INPUT_VERSION}\"/" pyproject.toml | |
| sed -i "s/__version__ = \".*\"/__version__ = \"${INPUT_VERSION}\"/" src/splunk_ao_a2a/_version.py |
🤖 Generated by Astra
There was a problem hiding this comment.
Good catch — switched from grep -Eq to bash [[ =~ ]] across all three PyPI release workflows. The [[ =~ ]] operator anchors ^/$ to the entire string rather than matching per-line, so a multi-line input like 0.1.0\n<payload> is now rejected outright. Fixed in PR #59.
Summary
Adds
.github/workflows/release-splunk-ao-a2a.yaml— aworkflow_dispatchpipeline that builds and publishes thesplunk-ao-a2apackage to PyPI.How it works
0.1.0)splunk-ao-a2a/pyproject.tomlandsplunk-ao-a2a/src/splunk_ao_a2a/_version.pylocally (no commit back)python -m build(hatchling backend)Notes
ci-tests-splunk-ao-a2a.yamlpattern)release-splunk-ao-a2a.yaml, environment:pypi)Made with Cursor