- name
- release-management
- description
- Guide maintainers and contributors through branching, versioning, changelog updates, and publishing agentops-accelerator. Trigger when users ask about branching strategy, creating a release, version tagging, publishing to PyPI, updating the changelog, cutting a release, opening a PR, or syncing a fork. Common phrases include "cut a release", "how do I publish", "create release branch", "tag a version", "update changelog", "release process", "bump version", "what branch should I use", "feature branch", "prepare release".
# Release Management
## Purpose
Guide contributors and maintainers through the AgentOps branching strategy, versioning conventions, changelog lifecycle, and PyPI release process.
## When to Use
- User asks what branch to base work on or where to raise a PR.
- User asks how to create a feature or release branch.
- User asks how to prepare a release or cut a version.
- User asks how to update the changelog.
- User asks how to tag a version or publish to PyPI.
- User asks how to sync their fork after a release.
- Instructions about branching or versioning are ambiguous.
## Branching Model
| Branch | Purpose |
|---|---|
| `main` | Always stable and deployment-ready. Only receives merges from `release/vx.y.z` branches. |
| `develop` | Integration branch. All feature PRs target here. |
| `release/vx.y.z` | Created by maintainers from `develop` when a release is ready to ship. |
| `feature/<name>` | Created by contributors from `develop` for all new work. |
**Default rule:** unless explicitly told otherwise, all work starts from `develop`.
## Feature Development Workflow
### Branch naming
```
feature/<short-description>
```
Examples: `feature/conversation-metadata`, `feature/add-evaluation-logging`
### Flow
1. Start from `develop`
2. Create `feature/<name>`
3. Implement changes
4. Commit with [conventional commit messages](#commit-guidelines)
5. Open PR → `develop`
### PR contract
- Source: `feature/*`
- Target: `develop`
- Never open a feature PR directly to `main`
## Release Workflow (Maintainers)
### Release branch naming
```
release/vx.y.z
```
Examples: `release/v2.4.2`, `release/v0.2.0`
### Flow
**Preferred: One-click via Cut Release workflow**
1. Confirm `develop` is green (CI passes) and all intended changes are merged.
2. Go to **Actions** tab → **Cut Release** → **Run workflow** → enter version (e.g. `0.2.0`, no `v` prefix).
3. The workflow automatically:
- Creates `release/v0.2.0` from `develop`
- Updates `CHANGELOG.md` (adds versioned section `[0.2.0] - YYYY-MM-DD`)
- Pushes the branch (triggers staging pipeline automatically)
- Opens a PR: `release/v0.2.0` → `main`
4. Wait for staging to pass (build → TestPyPI → verify, plus parallel `build-vsix`).
Download and manually evaluate the pre-release VSIX artifact. Staging uploads
Python to TestPyPI but never uploads to Marketplace or reserves a Marketplace
version. No Marketplace environment or approval is required during staging.
5. Get the PR reviewed and merge into `main`.
6. Tag the release on `main` **and sync `develop` in the same sitting**. Tagging
publishes to PyPI immediately; there is no approval prompt. Leaving `develop`
behind `main` corrupts the next release's CHANGELOG.
```bash
git checkout main
git pull origin main
git tag v0.2.0
git push origin v0.2.0
git checkout develop
git pull origin develop
git merge main
git push origin develop
# MUST print nothing:
git fetch origin && git log --oneline origin/develop..origin/main
```
After the merge, open `CHANGELOG.md` and confirm everything under
`## [Unreleased]` is genuinely unreleased. Git happily places the incoming
`## [0.2.0]` heading above develop's unreleased entries, nesting new work
inside a shipped version.
7. Watch the Release workflow finish (build → TestPyPI → verify → publish-pypi →
publish-vsix → github-release). The Python `release` environment has no
protection rules: Python publishes **before Marketplace approval**. A human
must approve `marketplace-release` for stable extension publication; do not
bypass that gate. GitHub Release waits for successful Marketplace publication.
8. Delete the release branch:
```bash
git push origin --delete release/v0.2.0
git branch -d release/v0.2.0
```
**Alternative: Manual release branch creation**
1. Confirm `develop` is green.
2. Create release branch from `develop`:
```bash
git checkout develop
git pull origin develop
git checkout -b release/v0.2.0
```
3. Update `CHANGELOG.md` - see [Changelog Lifecycle](#changelog-lifecycle) below.
4. Commit and push:
```bash
git add CHANGELOG.md
git commit -m "chore: prepare release 0.2.0"
git push origin release/v0.2.0
```
This triggers the staging pipeline automatically.
5. Open PR: `release/v0.2.0` → `main`.
6. After staging passes and review is complete, merge to `main`.
7. Tag and push, then immediately sync `develop` and verify with
`git log --oneline origin/develop..origin/main` (must be empty). Pushing the
tag publishes to PyPI with no approval prompt. Same commands and same
CHANGELOG check as step 6 above.
```bash
git checkout main
git pull origin main
git tag v0.2.0
git push origin v0.2.0
```
8. Delete the release branch (same as above).
### Release PR contract
- Source: `release/vx.y.z`
- Target: `main`
- Do NOT introduce new feature work in a release branch - only changelog updates.
## Versioning Rules
Follow [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`
| Type | When to use |
|---|---|
| `PATCH` | Bug fixes and minor backward-compatible improvements |
| `MINOR` | New backward-compatible features |
| `MAJOR` | Breaking changes to the CLI contract or output schema |
Version numbers follow a consistent pattern across artifacts. The git tag and GitHub Release use a `v` prefix. The release branch also uses the `v` prefix. Versioning is fully automatic via **setuptools-scm** - there is no `version` field in `pyproject.toml`.
| Artifact | Format | Example |
|---|---|---|
| Release branch | `release/vx.y.z` | `release/v2.4.2` |
| `pyproject.toml` | `dynamic = ["version"]` | Version derived from git tags via setuptools-scm |
| Git tag / GitHub Release | `vx.y.z` | `v2.4.2` |
| Changelog heading | `## [x.y.z] - YYYY-MM-DD` | `## [2.4.2] - 2026-03-22` |
**Never add `version = "..."` to `pyproject.toml`** - this will conflict with setuptools-scm.
### Version on `develop`
- The version on `develop` is derived automatically by setuptools-scm (e.g., `0.1.3.dev12`).
- Do NOT preemptively bump any version on `develop` for an upcoming release.
- Feature branches should not modify `pyproject.toml` version.
## Changelog Lifecycle
The changelog follows a two-phase lifecycle: development on `develop`, finalization on `release/vx.y.z`.
### Development phase (`develop`)
- Add all user-visible changes under the next versioned section at the top of the changelog.
- Do NOT preemptively assign a future version number on `develop`.
- Do NOT create empty version sections.
```markdown
## [0.2.0] - 2026-04-20
### Added
- New orchestration strategy for multi-turn evaluations.
### Fixed
- Corrected resource cleanup order in Foundry backend shutdown.
```
### Release phase (`release/vx.y.z`)
When creating the release branch, the cut-release workflow inserts a versioned section header with the release date. Verify the changelog entries are correct and complete.
All release artifacts must be in sync:
| Artifact | Value |
|---|---|
| Release branch | `release/v2.4.2` |
| Changelog heading | `## [2.4.2] - YYYY-MM-DD` |
| Git tag / GitHub Release | `v2.4.2` |
### Changelog sections
Use when applicable: `Added`, `Changed`, `Fixed`, `Removed`, `Deprecated`, `Security`.
### Writing style
- Start each entry with a **bold title**, followed by a brief technical explanation.
- Explain what changed and why it matters - include relevant technical context.
- Avoid vague wording: no "minor updates", "improvements", or "fixes" as standalone entries.
### Safety rules
- Never assign a release version on `develop` prematurely.
- Never leave a release branch without a properly dated versioned entry.
- Never mismatch version numbers across branch name, changelog, and tag.
- Never leave `develop` behind `main` after a release. `cut-release.yml` branches
from `develop` and replaces the `## [Unreleased]` marker exactly once, so a
stale `develop` republishes shipped entries and drops the previous version's
section when the next release PR merges into `main`.
## Commit Guidelines
Use conventional commit format:
```
feat: add conversation metadata support
fix: correct chat history persistence issue
docs: update changelog for 2.4.2
chore: prepare release 2.4.2
```
## Publishing Authentication
Both `staging.yml` and `release.yml` publish through
[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) using
`id-token: write`. There is no PyPI or TestPyPI API token stored in this repo.
| Secret | Scope | Purpose |
|---|---|---|
| `RELEASE_PAT` | repository | Used by `cut-release.yml` to open the release PR |
Neither the `staging` nor the `release` environment holds any secret. Confirm with
`gh api repos/Azure/agentops/actions/secrets --jq '.secrets[].name'`.
Trusted Publishing is configured on the index side (pypi.org and test.pypi.org →
**Manage → Publishing**) and must match the repository, workflow filename, and
environment name exactly. A mismatch fails with `403` at upload time.
### Marketplace: dedicated Entra OIDC identity
Marketplace publishing jobs use a **dedicated user-assigned managed identity (UAMI)**,
`azure/login@v3` with `allow-no-subscriptions: true`, and `id-token: write`.
No Azure RBAC grant is needed solely to publish. Publisher membership supplies
that permission. Do not change Python Trusted Publishing, shared `AZURE_*` E2E
variables, repository-wide OIDC configuration, or the GitHub `RELEASE_PAT`.
Marketplace publication is **stable-only**, through the production `vX.Y.Z`
tag flow. Previously staging could publish pre-release `X.Y.Z`, causing stable
publication of the same version to skip as a duplicate. Artifact-only staging
eliminates that collision; `package --pre-release` reserves no cloud version.
`marketplace-release` is the only required publishing environment, separate
from Python `staging`/`release`. Retain its required human reviewers and selected
branch/tag deployment policies **before enabling identity variables**:
| Environment | Allowed deployment refs |
| --- | --- |
| `marketplace-release` | Tags `v*`; optionally protected `main` for manual dispatch with tag input |
| `marketplace-staging` | Existing policy; optional legacy/read-only preflight context |
| `marketplace-validation` | Existing approved validation-ref policy; optional read-only preflight context |
The staging and validation Marketplace environments were created earlier, not
deleted by this change. Neither is required to stage/package or adds a staging
approval. `build-vsix` has no environment, `id-token`, Azure login, or profile
variables; it only produces a VSIX artifact for manual evaluation.
Stable manual job guards allow only `main` or the same release tag as the input.
An environment subject alone does not restrict branches; protections are essential.
For an authorized pre-migration-tag retry, explicitly dispatch the **new migrated
workflow on protected `main`** with a valid release tag. The Marketplace job
checks out tooling from `github.workflow_sha` at the workspace root and extension
source from `refs/tags/<tag>` into `release-source/`: old extension source uses
the new OIDC action/helper. Tag-based dispatch must match the tag input.
Re-running a historical old workflow still executes its old PAT code, not the
migrated workflow. A retry is a real publication attempt.
The publishing environment needs these variables (optional diagnostic
environments need them only when their preflight is used):
- `MARKETPLACE_AZURE_CLIENT_ID`: UAMI client GUID.
- `MARKETPLACE_AZURE_TENANT_ID`: approved identity tenant GUID.
- `MARKETPLACE_PROFILE_ID`: Marketplace `profiles/me` profile `id`,
**not** the Entra principal/object ID.
Verify customization read-only with
`gh api repos/Azure/agentops/actions/oidc/customization/sub`. The verified ordered
claim keys are `repository_owner_id`, `repository_id`, `context` with
`use_default: false`. Federation must use:
- Issuer: `https://token.actions.githubusercontent.com`
- Audience: `api://AzureADTokenExchange`
- Release subject: `repository_owner_id:6844498:repository_id:1161883340:environment:marketplace-release`
- Optional legacy staging preflight subject: `repository_owner_id:6844498:repository_id:1161883340:environment:marketplace-staging`
Resolve the UAMI Marketplace profile using a CLI token for resource
`499b84ac-1321-427f-aa17-267ca6975798` and a read-only request to
`https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1`.
Record only the profile `id`; never print or store token values. A publisher
Owner must grant that profile **Contributor, not Owner**, on `AgentOpsAccelerator`.
### Shared helper and local publishing
- Requires Python 3.11+ (stdlib), Azure CLI, and Node 22 with `vsce >=3.9.2`
for publishing. Workflows pin `npm install -g @vscode/vsce@3.9.2`.
- `python scripts/marketplace.py check` is read-only: it selects
`MARKETPLACE_AZURE_TENANT_ID`, pins the self profile to
`MARKETPLACE_PROFILE_ID`, and checks explicit Contributor/Owner/Creator
membership with zero deny permissions. A generic HTTP 200 is not sufficient.
- `python scripts/marketplace.py discover --out marketplace-profile.json`
bootstraps the Marketplace profile without an expected ID or publisher access.
Only tenant/profile IDs are written, never tokens.
- `marketplace-preflight.yml` provides discover/check via manual dispatch or a
reviewed reusable-workflow caller. It never publishes and retains
validation/staging/release environment choices for diagnostics. Respect the
selected environment's existing ref policy and human review; no new
diagnostic environment or approval is required by staging. Never create
dummy release refs. A validation/staging-context result does not prove
release-context authentication or actual stable publication.
- `python scripts/marketplace.py publish --package-path PATH [--pre-release] [--allow-already-exists]`
preflights before uploading. CI opts into already-existing-version handling;
local defaults fail. The flag maps to native `vsce --skip-duplicate`
(existing-version/409 only), not output substring matching; other errors
always propagate. The child environment clears inherited PAT and
EnvironmentCredential variables and selects the tenant. **No PAT fallback.**
Low-level `--pre-release` support is not part of the normal release flow.
- Local `scripts/staging.sh` and `scripts/staging.ps1` run lint/tests, Python
build, TestPyPI upload, smoke verification, and VSIX artifact packaging only.
They perform no Azure preflight or Marketplace publication. For pure
no-upload packaging use `uv build` and extension `npm run package:prerelease`;
the staging scripts still upload Python to TestPyPI.
- Local release scripts preflight before side effects when `vsce` exists, then
publish stable. Their prior missing-`vsce` extension-packaging skip remains
unchanged and does not prove Marketplace access.
Use `check` alone for no-upload validation. First run
`az login --tenant <publisher-identity-tenant> --allow-no-subscriptions` and set
`MARKETPLACE_AZURE_TENANT_ID` and `MARKETPLACE_PROFILE_ID` for **your interactive
publishing identity**, not the UAMI. Your identity needs publisher Contributor
or Owner membership. Profile pinning prevents wrong account/tenant publishing.
- Extension `npm run publish` packages a stable VSIX before calling the shared
helper, which preflights before upload. It requires
Python 3.11+ and the helper from the repository checkout.
- `npm run publish:prerelease` is removed. `npm run package:prerelease` remains
artifact-only with `package --pre-release`, not a Marketplace upload.
- CLI profile and role preflight success is **not proof of actual upload**.
### Staged rollout (not completed by merging code)
1. Confirm permanent ownership, approved production tenant/subscription
placement, and operational responsibility under existing policy outside code
rollout. The earlier non-production
personal-subscription probe is feasibility evidence, not policy approval.
2. Verify the dedicated UAMI, exact release federation, publisher Contributor
membership, `marketplace-release` reviewers and deployment policies, then
variables. Legacy diagnostic environments are not staging prerequisites.
3. Run an authorized permission-only preflight (`check`, no publishing scripts).
Use `marketplace-preflight.yml`: discover the profile, grant Contributor,
then check its explicit publishing role. A local interactive `check` alone
does not validate CI federation. Do not bypass required human approvals.
4. Verify a legitimate staging candidate's TestPyPI upload and artifact-only
VSIX for manual evaluation. No Marketplace pre-release publication is needed.
5. Explicitly authorize a **legitimate** stable release, obtain human
`marketplace-release` approval, and verify actual Marketplace publication.
6. **Only after actual stable publication succeeds and remaining consumers are
checked**, remove the legacy GitHub `VSCE_PAT`. Its owner must confirm no
other consumers before revoking the underlying Azure DevOps PAT.
Include historical workflow re-runs in that review; retire old PAT paths
and use the migrated workflow on `main` for authorized old-tag retries.
**Never touch `RELEASE_PAT`.**
GitHubで見る