Skip to content

chore(workflow): added release workflow#2940

Open
poshinchen wants to merge 1 commit into
strands-agents:mainfrom
poshinchen:chore/release-workflow
Open

chore(workflow): added release workflow#2940
poshinchen wants to merge 1 commit into
strands-agents:mainfrom
poshinchen:chore/release-workflow

Conversation

@poshinchen

@poshinchen poshinchen commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

Manually-triggered, per-package release pipeline. Pins main to a SHA, runs gates against it, drafts AI release notes via Bedrock, pauses on a 2-reviewer approval, creates the tag + GH release, then publishes to PyPI / NPM via OIDC trusted publishing. The existing python-pypi-publish-on-release.yml / typescript-npm-publish-on-release.yml workflows are demoted to workflow_dispatch-only manual fallback paths.

Two design properties:

  • Artifact identity: the smoke-test workflow uploads its wheel / tarball; the inspect step re-uploads it as the verified bundle; publish downloads that bundle. The bytes a reviewer approves are the bytes that ship — no second build between approval and publish.
  • Fork-testability: registry publish is the only step requiring upstream-only trust. Fork the repo, dispatch with dry_run: false, and the whole pipeline runs end-to-end — only publish-pypi / publish-npm fails (registry rejects the fork's OIDC token). The build artifact is on the run page for local install + smoke testing. This is how we iterate on release.yml itself.

Pipeline (release.yml)

scan-commits
├─► py-test-lint / ts-check / ts-test       (NEW: parity with old publish-on-release gates)
├─► integ-python / integ-typescript          (forks can choose to run or not, but credentials configuration is on the owner)
├─► py/ts-security-audit
└─► py/ts-package-build                       (uploads pypi-build-output / npm-build-output)
        └─► py/ts-inspect                     (NEW: twine check, list contents, re-upload as pypi-dist-bundle / npm-pack-bundle)
                └─► draft-summary             (AWS configure step now continue-on-error so forks degrade to git shortlog)
                        └─► approve-release   (auto-passes on forks with no env reviewers)
                                └─► create-gh-release    (gated on a publish-* success, skipped on forks)
                                        └─► publish-pypi / publish-npm  ◄── THE step that fails on forks
  • scan-commits pins release_sha = origin/main, validates the typed version, rejects duplicate / non-monotonic / python/v2+ tags.
  • create-gh-release runs before publish so the tag is the source of truth — a publish failure leaves the tag intact and only the publish job needs re-running. On forks it runs but no-ops its gh release create step.
  • Integ tests auto-skip on forks; opt-in via run_integ_on_fork: true for fork users with their own AWS credentials.
  • draft-summary's AWS configure step is continue-on-error: true, so a fork without secrets falls back to git shortlog cleanly.

Supporting changes

  • .github/scripts/draft-release-notes.py (new) — Bedrock-drafted {bump, notes}. Bump is advisory; maintainer types the version at dispatch.
  • python-security-audit.yml (new) — pip-audit against the full dependency closure. Mirrors typescript-security-audit.yml.
  • python-test-package-build.yml (new) — wheel install + import in an out-of-tree venv. Uploads dist as pypi-build-output for the inspect step.
  • typescript-test-package-pack.yml — also uploads the packed tarball.
  • python-integration-test.yml / typescript-integration-test.yml — added workflow_call(ref) + a validate-call-ref job that rejects refs/pull/* / owner:branch refs. Dropped the prior repository.fork == true hard-reject; a fork using its own secrets on its own code is a legitimate use case (the ref-shape case statement is the real protection against ref-injection).
  • python-pypi-publish-on-release.yml / typescript-npm-publish-on-release.yml — trigger flipped to workflow_dispatch(tag). They no longer fire on release: published (that path is owned by release.yml). Header banner marks them as emergency fallback.

One-time setup before merging

  • Create environments in repo settings:
    • release-python, release-typescript: required_reviewers: 2, deployment branches → main only.
    • pypi, npm: allow release.yml.
  • Register release.yml as a trusted publisher:
    • PyPI for strands-agents: workflow release.yml, environment pypi.
    • npmjs.com for @strands-agents/sdk: workflow release.yml, environment npm.
  • Leave existing trusted-publisher entries for the fallback workflows in place.
  • AWS_ROLE_ARN secret needs bedrock:InvokeModel (already used by strands-command.yml).

Related Issues

Documentation PR

N/A — release pipeline is contributor-facing only.

Type of Change

Other: CI / release infrastructure.

Testing

  • actionlint + shellcheck pass on all modified workflows.
  • No Python source files changed; lint/format unaffected.

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions github-actions Bot added size/l area-community Related to community and contributor health chore Maintenance tasks, dependency updates, CI changes, refactoring with no user-facing impact strands-running labels Jun 24, 2026
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml
Comment thread .github/scripts/draft-release-notes.py Outdated
print(f"Could not parse Bedrock response as JSON: {exc}", file=sys.stderr)
return 2

bump = result.get("bump", "unknown")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: bump from the model is written straight to bump.txt without validating it's one of major/minor/patch. The docstring promises that constraint, and the run summary presents it as the advisory bump. A malformed/hallucinated value would flow through unchecked.

Suggestion: Low priority since it's advisory-only, but a quick if bump not in {"major","minor","patch"}: bump = "unknown" keeps the output honest with the documented contract.

Comment thread .github/workflows/release.yml
@github-actions

Copy link
Copy Markdown
Contributor

Issue: The PR description is the unfilled template — no summary, no linked issue, and the Testing section is blank. For a release pipeline that adds a workflow_call entry point to the secret-bearing pull_request_target integ workflows, reviewers need to know how this was exercised (the workflow-call trust model especially can't be validated by CI alone — see AGENTS.md: "Actually exercise the change, don't just rely on the gate").

Suggestion: Fill in the description with the why, link any tracking issue, and note how the pipeline was validated end to end (e.g. a dry-run in a fork/test repo). Even "couldn't fully exercise X because it needs the configured release environment" is useful to state explicitly.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Request Changes

Well-structured release pipeline with a genuinely thoughtful security model — the workflow_call trust invariant (fork rejection + fork-PR/cross-repo ref shape checks in validate-call-ref, plus the auth-skip justification) is documented carefully and reasons about the right threats. The SHA-pinning and atomic gh release create --target approach are also nice. A few correctness/operational gaps are worth resolving before merge.

Review themes
  • Publish-gate mismatch (most important): This pipeline allows arbitrary semver / major bumps, but python-pypi-publish-on-release.yml only fires for python/v1. — a Python major would tag + release but silently skip PyPI. Either align the gate or reject it loudly here.
  • Missing setup doc: RELEASE_WORKFLOW_SETUP.md is referenced but absent; the required environments/reviewers/branch restrictions are prerequisites for the pipeline to work.
  • Approval cost on dry runs: approve-release runs even for dry runs (the default), burning two reviewer approvals and conditioning reviewers to rubber-stamp.
  • Operational hardening: no timeout-minutes on the Bedrock/integ-bearing jobs; advisory bump isn't validated against its documented value set.
  • PR hygiene: description is the unfilled template — no why, no linked issue, no note on how the security-sensitive workflow_call change was exercised.

The security thinking behind the workflow_call entry point is the strongest part of this PR — nicely done.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review after force-push (a833c83) — thanks for the update. I diffed the new commit against the prior one; the code for all five outstanding items is unchanged, so the earlier review still stands:

  • ⚠️ Setup doc: thread was marked resolved, but RELEASE_WORKFLOW_SETUP.md is still missing and release.yml:28 still references it.
  • 🔄 Publish-gate mismatch (most important): scan-commits still has no guard against Python majors, while python-pypi-publish-on-release.yml only fires on python/v1..
  • 🔄 Dry-run approvals: approve-release still lacks if: inputs.dry_run != true.
  • 🔄 bump validation: draft-release-notes.py:159 still writes the model value unchecked.
  • 🔄 Timeouts: no timeout-minutes on any job.

Happy to dig into any of these if a recommendation is unclear — assessment remains Request Changes until the publish-gate and setup-doc items are addressed.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review after 5c53369 — all blocking items are resolved. 🎉

Prior finding Status
Publish-gate mismatch (Python major silent no-op) scan-commits now rejects python major >= 2 loudly with a message pointing at the v1. gate
Dangling RELEASE_WORKFLOW_SETUP.md reference ✅ Reference removed; setup checklist inlined into the release.yml header
Dry-run burning reviewer approvals approve-release now gated by if: inputs.dry_run != true
PR description was the unfilled template ✅ Thorough description with the why, per-step rationale, security model, and an explicit note on what can't be exercised until the environments exist

Two optional, non-blocking nits remain — neither needs to hold up merge:

  • bump validation (draft-release-notes.py:159): the model value is still written to bump.txt unverified. Advisory-only, so low risk; a one-line if bump not in {"major","minor","patch"}: bump = "unknown" would keep it honest with the docstring.
  • timeout-minutes: still absent on the Bedrock/integration jobs. A safety cap would prevent a hung run from holding resources, but it's a hardening nice-to-have.

Assessment: Approve — the publish-gate fix in particular turns the most dangerous failure mode into an explicit error. The two remaining nits are yours to take or leave.

- name: Install build tooling
run: pip install --no-cache-dir build

- name: Build wheel + sdist

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue (Critical): This wheel will be built with the wrong version, and 8ef8cb0 now publishes that wheel to PyPI.

strands-py/pyproject.toml uses hatch-vcs ([tool.hatch.version] source = "vcs", tag_regex = "^python/v(?P<version>.+)$"), so the version is derived from git describe --match "python/v*". But in the new pipeline:

  1. create-gh-release (which creates the python/vX.Y.Z tag) runs last, after publish-pypi.
  2. This checkout uses the default shallow depth with no fetch-tags: true.

So at python -m build time the only reachable tag is the previous release, and hatch-vcs produces a dev/post version like 1.3.1.dev5+g<sha> — never the maintainer's typed inputs.version. That mis-versioned wheel is exactly what py-inspectpublish-pypi uploads.

Suggestion: Tag-last ordering is fundamentally incompatible with VCS-derived versioning. Either create the tag on the pinned SHA before the build/publish jobs (then build from the tag with fetch-tags: true), or inject inputs.version explicitly at build time (e.g. SETUPTOOLS_SCM_PRETEND_VERSION / HATCH_VCS_PRETEND_VERSION-style override) so the wheel carries the intended release version. Worth validating end-to-end with a dry build that asserts strands.__version__ == inputs.version.

name: npm-pack-bundle
path: dist-npm

- name: Publish to NPM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue (Critical): This publishes strands-ts with the wrong version. strands-ts/package.json has "version": "0.0.1-development", and the new in-pipeline pack/inspect/publish path (ts-package-packts-inspectpublish-npm) has no step that sets the release version into package.json before npm pack.

The old typescript-npm-publish-on-release.yml had an explicit npm version "$VERSION" --no-git-tag-version step (lines 94–99) precisely for this — that step has no equivalent in the new flow. As written, npm publish would push the literal 0.0.1-development tarball (or fail/collide), and the maintainer's typed inputs.version never reaches the published package.

Suggestion: Add an npm version ${{ inputs.version }} --no-git-tag-version step in ts-package-pack (working-directory strands-ts) before the build/pack, mirroring the fallback workflow, so the verified tarball carries the intended version. A guard in ts-inspect that asserts the embedded package.json version equals inputs.version would catch this class of bug going forward.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review after 8ef8cb0 — this push is a substantial architectural rework, not just the two outstanding nits, so I re-reviewed the whole pipeline.

First, the good news — the previously-blocking items are all still fixed, and the new direction is sound in concept:

  • ✅ The publish model moved in-pipeline (OIDC trusted publishing) with build → twine check/tarball inspection → publish → tag-last, and the old release: published listeners were correctly demoted to manual workflow_dispatch fallbacks, so no double-publish. Nicely handled.
  • ✅ Publish-gate guard, dry-run approval gate, and the setup-doc reference fix are all intact.

However, the reordering introduced two new critical correctness bugs around version derivation (details in the two inline comments):

Critical: published artifacts carry the wrong version
  • Pythonhatch-vcs derives the version from the git tag, but create-gh-release (which creates the tag) now runs last, and the build checkout has no tags fetched. The wheel gets a …dev+g<sha> version off the previous tag and that's what publish-pypi uploads.
  • TypeScript — the new pack/publish path dropped the npm version $VERSION step the old workflow had, so npm publish would push the literal 0.0.1-development from package.json.

Common root cause: inputs.version is validated in scan-commits but never injected into either build — the build jobs only receive the SHA ref.

Two prior non-blocking nits also remain (advisory bump still unvalidated; no timeout-minutes anywhere) — still optional.

Assessment: Request Changes — the version-derivation bugs mean a release would publish a mis-versioned package to a public registry, which is worth fixing before merge. The overall pipeline shape is good; this is about getting the typed version onto the built artifact (and ideally an inspect-time assertion that catches it). Happy to dig in if any recommendation is unclear.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review after eb0bf75 — I diffed this push against the prior commit.

What's new and good:

  • ✅ Added the run_integ_on_fork input with correct gating (fork != true || run_integ_on_fork == true on both integ jobs), plus expanded fork-testability docs including the trusted-publisher setup note pointing at release.yml. Nice ergonomic improvement for iterating on the pipeline from a fork.

However, both critical version-derivation bugs from the last review are unchanged — this push didn't touch the build path:

  • 🔴 Python (python-test-package-build.yml): still raw python -m build with a shallow checkout and no tag/HATCH_VCS_PRETEND_VERSION injection, and create-gh-release still runs last. The published wheel will carry a …dev+g<sha> version off the previous tag, not the typed inputs.version.
  • 🔴 TypeScript (ts-package-pack): still no npm version $VERSION --no-git-tag-version step — the only occurrence of npm version in the repo remains the old fallback workflow. npm publish would push the literal 0.0.1-development from package.json.

Root cause is still that inputs.version is validated in scan-commits but never passed to the build jobs (they only receive ref). The two inline threads have the specific fixes.

The two prior non-blocking nits (advisory bump unvalidated, no timeout-minutes) also remain — still optional.

Assessment: Request Changes — the fork-testing change is a good addition, but a real release still publishes a mis-versioned package, so the two version bugs remain blocking. Happy to clarify either fix.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review after ff7cc64 — diffed against the prior commit.

What's new and good:

  • create-gh-release now runs before publish-pypi/publish-npm (both needs: create-gh-release), so the python/vX.Y.Z tag exists at publish time — good move on the ordering.
  • ✅ Added fetch-depth: 0 + fetch-tags: true to scan-commits and draft-summary.

Unfortunately both critical version bugs are still present, because the fix targeted the wrong stage. The reorder addresses when the tag exists relative to publish, but the artifacts aren't built at publish time — they're built much earlier:

  • The wheel/tarball are produced in py-package-build (line 295) and ts-package-pack (line 305), which both needs: scan-commits only and build from release_sha before create-gh-release runs. publish-pypi/publish-npm merely download that already-built, immutable bundle. So the tag now existing at publish time doesn't change the bytes — the wheel was already stamped with the wrong version several jobs earlier.
  • The new fetch-tags: true is on scan-commits/draft-summary, not on the called build workflows (python-test-package-build.yml / typescript-test-package-pack.yml). Those still check out shallow with no tags, so hatch-vcs still derives …dev+g<sha> off the previous tag, and the TS path still has no npm version $VERSION step (the only occurrence remains the old fallback workflow at line 99).

The fix still needs to land at the build step, one of:

  1. Inject the typed version into the build directly — HATCH_VCS_PRETEND_VERSION: ${{ inputs.version }} for Python and npm version ${{ inputs.version }} --no-git-tag-version for TS (simplest, no reordering needed); or
  2. Create the tag before the build jobs and have the build workflows check out with fetch-tags: true so hatch-vcs sees it.

Either way, passing inputs.version into the build jobs (they currently only receive ref) is the missing link. An inspect-time assertion that strands.__version__ == inputs.version / package.json.version == inputs.version would catch this class of bug going forward.

The two prior nits (advisory bump unvalidated, no timeout-minutes) remain — optional.

Assessment: Request Changes — the tag-before-publish reorder is a genuine improvement, but a real release would still publish a mis-versioned artifact because the build precedes tagging and no version is injected. Happy to clarify either fix path.

@github-actions

Copy link
Copy Markdown
Contributor

Re-review after 3bba66b — both critical version bugs are fully resolved. 🎉

The fix landed exactly where it needed to (at the build step) and went a step further with inspect-time guards:

  • Pythonpython-test-package-build.yml now takes a version input, exports it as HATCH_VCS_PRETEND_VERSION before python -m build, and the clean-venv step asserts strands.__version__ == expected. A …dev+g<sha> build would now fail loudly instead of silently shipping.
  • TypeScriptts-package-pack now takes a version input, runs npm version $VERSION --no-git-tag-version before pack, and asserts the packed tarball's package.json version matches (via tar … | jq). The 0.0.1-development tarball can no longer reach npm publish.
  • release.yml wires version: ${{ inputs.version }} into both build jobs, so the typed release version finally reaches the artifact. Combined with the prior tag-before-publish reorder + fetch-tags, the version story is now consistent end-to-end.
  • ✅ Nicely done keeping it backward-compatible — PR-time callers omit version, the stamp/assert steps are skipped, and the existing import/install smoke test is unchanged.

Only the two long-standing optional nits remain (no timeout-minutes on any job; advisory bump still unvalidated) — both take-it-or-leave-it, not blocking.

Assessment: Approve — every blocking issue raised across the review cycle is resolved and no new issues were introduced. The version-injection + assertion approach is robust and self-guarding. Nice work seeing this through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-community Related to community and contributor health chore Maintenance tasks, dependency updates, CI changes, refactoring with no user-facing impact size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant