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
11 changes: 11 additions & 0 deletions .github/workflows/release-notes-cut-advanced.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ name: Cut Release Notes (Advanced)
on:
workflow_dispatch:
inputs:
repo:
description: "Target repository (valkey-io org)"
required: true
type: choice
default: valkey
options:
- valkey
- valkey-search
- valkey-json
- valkey-bloom
version:
description: "Target MAJOR.MINOR.PATCH"
required: true
Expand Down Expand Up @@ -66,6 +76,7 @@ jobs:
id-token: write
uses: ./.github/workflows/release-notes-cut.yml
with:
repo: ${{ inputs.repo }}
version: ${{ inputs.version }}
stage: ${{ inputs.stage }}
urgency: ${{ inputs.urgency }}
Expand Down
97 changes: 82 additions & 15 deletions .github/workflows/release-notes-cut.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Cut a Valkey release: generate notes all at once + bump version + carry prior RCs.
# Cut a release: generate notes all at once + bump version + carry prior RCs.
#
# The normal dispatch asks only for the release decision: version, stage, urgency,
# and whether to preview. Patch versions infer stage=ga when stage is left empty.
# Supports valkey core and the module repos (valkey-search, valkey-json,
# valkey-bloom); per-repo conventions live in scripts/release_notes/projects.py.
#
# The normal dispatch asks only for the release decision: repo, version, stage,
# urgency, and whether to preview. Patch versions infer stage=ga when stage is
# left empty.
# Advanced overrides live in release-notes-cut-advanced.yml, which calls this same
# workflow through workflow_call so both paths execute identical release code.
#
Expand All @@ -15,6 +19,16 @@ name: Cut Release Notes
on:
workflow_dispatch:
inputs:
repo:
description: "Target repository (valkey-io org)"
required: true
type: choice
default: valkey
options:
- valkey
- valkey-search
- valkey-json
- valkey-bloom
version:
description: "Target MAJOR.MINOR.PATCH, which must advance the current release line (e.g. 9.1.1 after 9.1.0)"
required: true
Expand All @@ -40,6 +54,11 @@ on:
default: true
workflow_call:
inputs:
repo:
description: "Target repository name in the valkey-io org (valkey, valkey-search, valkey-json, valkey-bloom)"
required: false
type: string
default: valkey
version:
description: "Target MAJOR.MINOR.PATCH"
required: true
Expand Down Expand Up @@ -90,9 +109,11 @@ on:
default: true
secrets:
VALKEYRIE_BOT_APP_ID:
required: true
required: false
VALKEYRIE_BOT_PRIVATE_KEY:
required: true
required: false
VALKEY_GITHUB_TOKEN:
required: false
AWS_ROLE_ARN:
required: true

Expand All @@ -109,41 +130,65 @@ jobs:
contents: read
id-token: write
concurrency:
# Serialize every stage of a target version. This also makes an inferred patch
# GA, an explicit ga, and case variants share one lock.
group: release-cut-${{ inputs.version }}
# Serialize every stage of a target version per repository. This also makes
# an inferred patch GA, an explicit ga, and case variants share one lock.
group: release-cut-${{ inputs.repo }}-${{ inputs.version }}
cancel-in-progress: false
env:
AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
CLAUDE_CODE_USE_BEDROCK: "1"
# secrets.* is not readable in step `if:` expressions; surface presence
# here so the token steps can be skipped on a fork without App creds.
HAS_APP_CREDS: ${{ secrets.VALKEYRIE_BOT_APP_ID != '' && secrets.VALKEYRIE_BOT_PRIVATE_KEY != '' }}
steps:
- name: Check out agent repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 1

# Gate the target repo to the exact supported set BEFORE any App token is
# minted for it and before it flows into RELEASE_NOTES_REPO (and from
# there into git clone/push targets). The dispatch UI is a choice list,
# but workflow_call passes any string.
- name: Validate target repository
shell: bash
env:
RELEASE_NOTES_TARGET_REPO_NAME: ${{ inputs.repo }}
run: |
set -euo pipefail
case "${RELEASE_NOTES_TARGET_REPO_NAME}" in
valkey|valkey-search|valkey-json|valkey-bloom) ;;
*)
echo "Invalid repo (want valkey, valkey-search, valkey-json, or valkey-bloom): ${RELEASE_NOTES_TARGET_REPO_NAME}" >&2
exit 1
;;
esac

- name: Set up agent (Python deps + Claude Code)
uses: ./.github/actions/setup-agent
with:
install-claude: "true"

# Two token steps, selected by whether this cut reads advisories, so a
# normal cut never depends on the advisory grant. repository-advisories:read
# normal cut never depends on the advisory grant. Both steps are skipped
# when the App credentials are absent (a fork without VALKEYRIE_BOT_*):
# the cut step then falls back to VALKEY_GITHUB_TOKEN for READ-ONLY dry
# runs and refuses to publish. repository-advisories:read
# is only requested when --security-from-advisories is on; create-github-app-token
# fails if the App installation lacks a requested permission, so requesting it
# unconditionally would break EVERY cut (including ones that never touch
# advisories) whenever the grant is absent. Exactly one step runs; the token
# is read as (default || advisory) below, and a skipped step yields "".
- name: Generate GitHub App token
id: generate-token
if: ${{ !inputs.security_from_advisories }}
if: ${{ !inputs.security_from_advisories && env.HAS_APP_CREDS == 'true' }}
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.VALKEYRIE_BOT_APP_ID }}
private-key: ${{ secrets.VALKEYRIE_BOT_PRIVATE_KEY }}
owner: valkey-io
repositories: valkey
repositories: ${{ inputs.repo }}
# contents:write to push the agent-namespaced prep branch;
# pull-requests:write to open the release PR; metadata:read is always required.
permission-contents: write
Expand All @@ -152,13 +197,13 @@ jobs:

- name: Generate GitHub App token (with advisories)
id: generate-token-advisories
if: ${{ inputs.security_from_advisories }}
if: ${{ inputs.security_from_advisories && env.HAS_APP_CREDS == 'true' }}
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.VALKEYRIE_BOT_APP_ID }}
private-key: ${{ secrets.VALKEYRIE_BOT_PRIVATE_KEY }}
owner: valkey-io
repositories: valkey
repositories: ${{ inputs.repo }}
# Same as above plus repository-advisories:read, which --security-from-advisories
# needs to list published advisories. This step only runs for an advisory
# cut, so a plain cut is never blocked when the App lacks this grant. The
Expand All @@ -182,8 +227,9 @@ jobs:
env:
# Exactly one token step ran (gated by security_from_advisories); the
# skipped one yields "", so this resolves to whichever produced a token.
RELEASE_NOTES_GITHUB_TOKEN: ${{ steps.generate-token.outputs.token || steps.generate-token-advisories.outputs.token }}
RELEASE_NOTES_REPO: valkey-io/valkey
RELEASE_NOTES_GITHUB_TOKEN: ${{ steps.generate-token.outputs.token || steps.generate-token-advisories.outputs.token || secrets.VALKEY_GITHUB_TOKEN }}
RELEASE_NOTES_DRY_RUN_FALLBACK_TOKEN: ${{ (steps.generate-token.outputs.token || steps.generate-token-advisories.outputs.token) == '' && secrets.VALKEY_GITHUB_TOKEN != '' && 'true' || 'false' }}
RELEASE_NOTES_TARGET_REPO_NAME: ${{ inputs.repo }}
RELEASE_NOTES_VERSION: ${{ inputs.version }}
RELEASE_NOTES_STAGE: ${{ inputs.stage }}
RELEASE_NOTES_URGENCY: ${{ inputs.urgency }}
Expand All @@ -196,6 +242,27 @@ jobs:
RELEASE_NOTES_DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
run: |
set -euo pipefail
# The "Validate target repository" step already enforced the allowlist
# (before token minting). Re-check only the shape here so this step
# never composes RELEASE_NOTES_REPO from an empty or path-like value.
if [[ ! "${RELEASE_NOTES_TARGET_REPO_NAME}" =~ ^[a-z][a-z-]*$ ]]; then
echo "Invalid repo name shape: ${RELEASE_NOTES_TARGET_REPO_NAME}" >&2
exit 1
fi
export RELEASE_NOTES_REPO="valkey-io/${RELEASE_NOTES_TARGET_REPO_NAME}"
# Fork testing: with no App credentials, a personal-token fallback
# (VALKEY_GITHUB_TOKEN) may run READ-ONLY dry runs. It must never
# publish: a PAT push would bypass the App's audited identity.
if [[ "${RELEASE_NOTES_DRY_RUN_FALLBACK_TOKEN}" == "true" && "${RELEASE_NOTES_DRY_RUN}" != "true" ]]; then
echo "Refusing a non-dry-run cut with the personal-token fallback;" \
"publishing requires the App credentials (VALKEYRIE_BOT_*)." >&2
exit 1
fi
if [[ -z "${RELEASE_NOTES_GITHUB_TOKEN}" ]]; then
echo "No token available: set VALKEYRIE_BOT_APP_ID/PRIVATE_KEY, or" \
"VALKEY_GITHUB_TOKEN for a dry run on a fork." >&2
exit 1
fi
# Validate a value used in a git ref position. These inputs flow into git
# commands as bare arguments (`git clone --branch <ref>`, `<ref>^{commit}`,
# push refspecs, `git compare` ends), so the gate must reject shapes that
Expand Down
71 changes: 53 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ New workflows are added as sibling directories to `backport/`. Each workflow pic
| Fuzzer Monitor | Active | Analyzes scheduled fuzzer runs and files issues for anomalous failures |
| CI Fix | Active | On-demand `@valkeyrie-bot fix <ci-link>` - diagnoses and fixes a failing test on a backport PR |
| Test Failure Detector | Active | Detects test failures from Daily CI, files/updates GitHub issues |
| Release Notes | Active | Cuts a release: AI-generates notes from `release-notes` PRs plus AI-triaged candidates without that label, promotes them onto a release line branch, bumps `src/version.h`, opens a PR (held as a draft when the cut flags issues) |
| Release Notes | Active | Cuts a release for valkey core or a module repo (search/json/bloom): AI-generates notes from `release-notes` PRs plus AI-triaged candidates without that label, promotes them onto a release line branch, bumps the repo's version file, opens a PR (held as a draft when the cut flags issues) |
| PR Reviewer | Planned | Two-stage code review with skeptic pass |
| Additional Daily CI Analysis | Planned | Detects flaky tests, generates fix PRs |

Expand Down Expand Up @@ -270,26 +270,55 @@ verify step. macOS verification runs once on its dedicated runner.

## Release Notes Workflow

Cuts a Valkey release in one shot. A maintainer dispatches the target version and
urgency, plus an explicit stage for `.0` releases; patch versions infer `ga`. The
agent derives the M.m release line and generates notes from the `release-notes`
Cuts a release in one shot for valkey core or a module repo (`valkey-search`,
`valkey-json`, `valkey-bloom`). A maintainer dispatches the target repo, version,
and urgency, plus an explicit stage for `.0` releases; patch versions infer `ga`.
The agent derives the M.m release line and generates notes from the `release-notes`
PRs plus candidates without that label that AI triage judges user-facing (Claude
via Bedrock). Deterministic release-impact checks keep crash, memory-safety,
corruption, access-control, protocol, compatibility, and similar fixes from being
silently excluded by an AI verdict. The agent renders the result onto the
long-running release line as a dated section, bumps `src/version.h`, refreshes
the running contributor list, and opens one PR for review (as a draft, holding
the merge, when the cut flags anything a maintainer should address first; see
[Edge-case handling](#edge-case-handling)).
long-running release line as a dated section, bumps the repo's version file,
refreshes the running contributor list, and opens one PR for review (as a draft,
holding the merge, when the cut flags anything a maintainer should address first;
see [Edge-case handling](#edge-case-handling)).
Nothing accumulates notes on a branch; the notes for a release are generated all
at once. The release line is changed only when a maintainer merges the generated
PR.

Per-repo conventions (changelog heading name, version file layout, note
categories, prompt wording) live in `scripts/release_notes/projects.py`:

| Repo | Version file | Stage recorded |
|---|---|---|
| valkey | `src/version.h` (`VALKEY_VERSION` macros) | yes |
| valkey-search | `src/version.h` (`kModuleVersion` + `MODULE_RELEASE_STAGE`) | yes |
| valkey-json | `CMakeLists.txt` (`project(... VERSION M.m.p)`) | no |
| valkey-bloom | `Cargo.toml` (`[package] version`) | no |

For repos whose version file records no stage, tag-based validation remains the
authoritative check against re-cutting an already-tagged stage. The
valkey-search 1.0 line (version inline in `src/module_loader.cc`) is not
supported and fails with a clear error.

Before automated cuts are enabled for a repository, its historical contributor
block must be normalized to the canonical cumulative footer used by this
workflow: a `### Contributors` heading followed by one `* Display Name @handle`
entry per line. Legacy repository-specific footer formats are migrated once in
the source repository rather than carried as permanent parsing rules here.

Unstable sentinels are repository-specific: valkey core uses
`255.255.255-dev`, valkey-json uses numeric `99.99.99`, and valkey-bloom uses
`99.99.99-dev`. The selected version bumper recognizes only its repository's
sentinel and replaces it with the requested version when a new release line is
cut.

The normal dispatch defaults to a read-only preview. For rc1 of a new minor line:

```bash
gh workflow run release-notes-cut.yml \
--repo valkey-io/valkey-ci-agent \
--field repo=valkey \
--field version=9.1.0 \
--field stage=rc1 \
--field urgency=LOW
Expand All @@ -301,20 +330,24 @@ stages differ only in `stage`; a patch release leaves it empty:
```bash
# Next RC (the 9.1.0-rc1 tag must exist on the 9.1 branch)
gh workflow run release-notes-cut.yml --repo valkey-io/valkey-ci-agent \
--field version=9.1.0 --field stage=rc2 --field urgency=LOW
--field repo=valkey --field version=9.1.0 --field stage=rc2 --field urgency=LOW

# GA after the final RC (the last rc tag must exist on the 9.1 branch)
gh workflow run release-notes-cut.yml --repo valkey-io/valkey-ci-agent \
--field version=9.1.0 --field stage=ga --field urgency=LOW
--field repo=valkey --field version=9.1.0 --field stage=ga --field urgency=LOW

# Patch GA (the 9.1.0 tag must exist on the 9.1 branch)
gh workflow run release-notes-cut.yml --repo valkey-io/valkey-ci-agent \
--field version=9.1.1 --field urgency=LOW
--field repo=valkey --field version=9.1.1 --field urgency=LOW

# Module repo patch GA (the 1.2.1 tag must exist on the 1.2 branch)
gh workflow run release-notes-cut.yml --repo valkey-io/valkey-ci-agent \
--field repo=valkey-search --field version=1.2.2 --field urgency=LOW
```

After reviewing the preview, repeat the same dispatch with
`--field dry_run=false` to open or update the release PR. The normal workflow
exposes only `version`, `stage`, `urgency`, and `dry_run`; `stage` is
exposes only `repo`, `version`, `stage`, `urgency`, and `dry_run`; `stage` is
case-insensitive and is required only when the patch component is zero.

If more changes merge into `M.m` while that release PR is open, dispatch the
Expand Down Expand Up @@ -386,10 +419,11 @@ human merges.
1. **Resolve the plan** (code) - normalize an explicit stage, or infer `ga` when
`PATCH > 0`; an omitted stage for `M.m.0` is rejected. Map that
`(version, stage)` onto the branch model above. The version is canonicalized
once (`M.m.p`, no leading zeros / stray whitespace) so `version.h`, the dated
heading, the commit title, and the branch names all agree. The requested state
must be newer than both `src/version.h` and every existing tag on that release
line; an already-released stage or downgrade is rejected before the AI runs.
once (`M.m.p`, no leading zeros / stray whitespace) so the repository's
version file, dated heading, commit title, and branch names all agree. The
requested state must be newer than both the version file and every existing
tag on that release line; an already-released stage or downgrade is rejected
before the AI runs.
2. **Discover the range** (code) - resolve `base..head` and walk it by graph
reachability, deduplicating to one entry per originating PR number. The M.m
branch tip is fetched once and pinned to an immutable SHA used by discovery,
Expand Down Expand Up @@ -429,7 +463,8 @@ human merges.
(`version_bump.py`), append the cumulative contributor list
(`contributors.py`) deduplicated by case-insensitive display-name/login
identity (PR-resolved logins give squash-merged authors proper @handles),
and bump `src/version.h`. These format primitives live
and bump the version file selected by the repository profile. These format
primitives live
in-repo rather than being imported from valkey, because upstream
`valkey-io/valkey` ships no such tooling, so a cut runs against unmodified
upstream (a plaintext `00-RELEASENOTES` placeholder and a `src/version.h`
Expand All @@ -452,7 +487,7 @@ explicit stage, an urgency outside `LOW/MODERATE/HIGH/CRITICAL/SECURITY`, or a
non-ISO date. Repository-state validation runs after the clone: an explicit
`--base-ref` that resolves to nothing aborts with a clear error, and a cut
against a non-existent M.m branch is refused immediately. A target that is equal
to or older than `src/version.h`, or at or behind an existing tag on that M.m
to or older than the repository's version file, or at or behind an existing tag on that M.m
line, is also refused before note generation.

When the cut raises anything a maintainer should address before merging, the
Expand Down
Loading