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
18 changes: 18 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!--
Pull request titles must follow conventional commits and are checked by CI:
feat|fix|docs|test|ci|chore(optional-scope): description
Append `!` (for example `feat(neutron)!:`) for a change that requires operator
action to upgrade.
-->

## What does this change do?

## Upgrade impact

- [ ] This change requires operator action to upgrade. If checked, add the
`upgrade-impact` label and a note under `Unreleased` in
`docs/release-notes/`. See [RELEASING.md](../RELEASING.md).

Operator action means anything a deployment has to do beyond a normal resync:
deploy repo or values changes, new or removed secrets, enabling or disabling a
component, or a manual one-time step.
73 changes: 73 additions & 0 deletions .github/workflows/release-note-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Release note check

# Deliberately has no `paths:` filter. A path-filtered workflow never reports a
# status on pull requests that do not match it, which means it can never be made
# a required check. Instead this always runs and short-circuits to success
# unless the pull request has been flagged as upgrade-impacting, so the majority
# of changes see no new friction.
on:
pull_request:
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
merge_group:
types: [checks_requested]

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
release-note:
name: Release note for upgrade-impacting change
runs-on: ubuntu-latest
permissions:
contents: read
steps:
# merge_group events carry no pull request context, so there is nothing to
# check. Succeed so this workflow is still safe to mark as required.
- name: Skip on merge queue
if: github.event_name == 'merge_group'
run: echo "No pull request context on merge_group, skipping."

- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
if: github.event_name == 'pull_request'
with:
fetch-depth: 0

- name: Require a release note when flagged
if: github.event_name == 'pull_request'
env:
# The title is attacker-controlled, so pass it through the environment
# rather than interpolating it into the script.
PR_TITLE: ${{ github.event.pull_request.title }}
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail

flagged=0
if printf '%s' "$PR_TITLE" \
| grep -Eq '^(feat|fix|docs|test|ci|chore)(\([^)]*\))?!:'; then
flagged=1
echo "Flagged by '!' in the pull request title."
fi
if printf '%s' "$PR_LABELS" | grep -q '"upgrade-impact"'; then
flagged=1
echo "Flagged by the upgrade-impact label."
fi

if [ "$flagged" -eq 0 ]; then
echo "Not flagged as upgrade-impacting, nothing to check."
exit 0
fi

if git diff --name-only "$BASE_SHA" "$HEAD_SHA" \
| grep -q '^docs/release-notes/'; then
echo "Release note present."
exit 0
fi

echo "::error::This pull request is flagged as upgrade-impacting but does not touch docs/release-notes/. Add a bullet to the Unreleased section of the current series page describing what an operator has to do, or remove the upgrade-impact label and the '!' from the title if no operator action is needed."
exit 1
8 changes: 5 additions & 3 deletions .markdownlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ MD013: false
MD014: false

# MD024 - Multiple headings with the same content
# Multiple identical headings in the document are not allowed
# This is disabled on all of the release notes pages in docs/release-notes
MD024: true
# Identical headings are only an error when they are siblings under the same
# parent. The release notes pages in docs/release-notes repeat headings such as
# "Action required" once per version section, which is intended.
MD024:
siblings_only: true

# MD033 - Inline HTML
# Triggered when raw HTML is used in a markdown document
Expand Down
199 changes: 199 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Releasing UnderStack

Repository-wide `vX.Y.Z` tags are created **manually**. No workflow creates
them. Pushing the tag triggers `containers.yaml` and `containers-openstack.yaml`,
which publish container images tagged with the git ref.

Per-artifact tags (`understackctl/vX.Y.Z`, `nautobotop-vX.Y.Z`,
`kubectl-us-net/vX.Y.Z`, `ironic-hardware-exporter/vX.Y.Z`, `dexop-vX.Y.Z`) are
released independently and are not covered here.

## Where upgrade notes live

Operator-facing upgrade notes live in `docs/release-notes/`, one page per minor
series, listing versions newest-first. Anyone whose change requires operator
action adds a bullet under `Unreleased` on the current series page **in the same
pull request as the change**, and labels the pull request `upgrade-impact`.

Two invariants:

- There is **exactly one `Unreleased` section** across all series pages, always
on the highest-numbered page.
- **Most releases will not need a note at all.** Tags are cut frequently and
usually carry nothing an operator has to act on. A version that requires no
operator action gets **no section**, and cutting it involves no docs change
whatsoever. Empty sections teach operators that these pages are not worth
opening, which is the one outcome that makes them useless.

Because `understack_ref` defaults to `HEAD` and the docs site only publishes
`main`, a note merged alongside its change is immediately visible to the
deployments that track `main`. That is the reason notes are written up front
rather than assembled at release time.

## Cutting a patch release

### 1. Review the diff for undocumented operator impact

Not every operator-affecting change gets flagged. Before promoting the notes,
compare the previous tag against `main` over the paths where impact hides:

```bash
git fetch --tags
git diff v0.4.25..main --stat -- \
charts/argocd-understack/values.yaml \
charts/argocd-understack/templates/ \
components/images-openstack.yaml
```

Look specifically for:

- new, renamed or removed `enabled:` keys under `global:` or `site:`
- OpenStack-Helm `chartVersion` bumps, especially ones crossing a release
series (for example `2025.2` to `2026.1`)
- OpenStack image series changes in `components/images-openstack.yaml`
- added or removed `application-*.yaml` templates, or new services added to the
service list in `application-openstack-helm.yaml`

Anything you find here that is not already under `Unreleased` is a gap. Add it
now.

### 2. Promote the `Unreleased` section

**If every subsection under `Unreleased` still says `_Nothing yet._`, there is
nothing to do here.** Leave the page alone and skip to step 3 to tag. This is
the common case.

Otherwise, in the current series page, for example `docs/release-notes/v0.4.md`:

1. Change `## Unreleased` to `## v0.4.26` and add the metadata lines
(`**Released:**`, `**Impact:**`, `**Applies to:**`).
2. Delete any subsection whose only content is `_Nothing yet._`.
3. Insert a fresh `## Unreleased` block above it, copied from the template
below.

Then open a pull request titled `docs: release notes for v0.4.26` and merge it
before tagging, so the tag contains its own notes.

### 3. Tag the release

Tag the current tip of `main`:

```bash
git checkout main && git pull
git tag -a v0.4.26 -m "v0.4.26"
git push origin v0.4.26
```

### 4. Create the GitHub release

Create the release from the tag with auto-generated notes. If this version got a
section in step 2, add one line at the top of the body pointing at it:

```markdown
**Upgrading?** See the
[v0.4.26 upgrade notes](https://rackerlabs.github.io/understack/release-notes/v0.4/#v0426).
```

If it got no section, leave the body as generated. Do not link a section that
does not exist.

### 5. Confirm the builds

Check that `containers.yaml` and `containers-openstack.yaml` succeeded for the
tag.

## Cutting a minor release

A new series always gets a page, whether or not the release itself has notes,
because that page is where subsequent `Unreleased` notes go. Same as above, but
first:

1. Create `docs/release-notes/v0.5.md` with a `# Release Notes: v0.5.x`
heading.
2. Move the `Unreleased` block out of `v0.4.md` into `v0.5.md`. If it has
content, promote that content to `## v0.5.0` and leave a fresh `Unreleased`
block above it. If it is empty, just leave the empty `Unreleased` block on
the new page. Either way `v0.4.md` ends up with no `Unreleased` section.
3. Add `- release-notes/v0.5.md` to `properdocs.yml` **above** the v0.4 entry.
Forgetting this fails the docs build, which is intentional.
4. Update the series table in `docs/release-notes/index.md`: mark v0.5.x
Current and v0.4.x Maintenance.
5. Update the `Unreleased` anchor in the "If you deploy from `main`" callout in
`docs/release-notes/index.md` to point at the new page.

## Section template

Copy this for a new version section. Include only the subsections that apply
and delete the rest: an empty `Rollback` heading is worse than no `Rollback`
heading.

Two authoring constraints in this repo. The `nl2br` extension is enabled, so a
single newline inside a paragraph renders as a line break. And markdownlint sets
`MD007: indent: 4`, so nested list items are indented by four spaces.

````markdown
## v0.4.26

**Released:** 2026-08-11
**Impact:** Action required
**Applies to:** site clusters

### Summary

One or two sentences on what changed and why an operator has to care. Link the
pull request or issue for the underlying detail.

### Action required

Ordered and copy-pasteable, in the order the steps must be performed.

1. Do the first thing.

```bash
kubectl -n openstack get pods
```

2. Do the second thing.

### Deploy repo changes

Show before and after, using the real path.

```yaml title="$CLUSTER_NAME/deploy.yaml"
site:
neutron:
enabled: true
```

### Secrets

- **New:** `<secret-name>` in namespace `<ns>`, keys `<k1>`, `<k2>`.
- **Removed:** `<secret-name>`, safe to delete after upgrading.

### Chart and image versions

- OpenStack-Helm `neutron` chart: `<old>` to `<new>`
(`charts/argocd-understack/values.yaml`).
- `keystone` image series: `<old>` to `<new>`
(`components/images-openstack.yaml`). Series tags are moving tags rebuilt
from `main`, and `pull_policy` is `Always`, so a resync pulls the current
build.

### Verification

How an operator confirms the upgrade worked.

```bash
kubectl -n openstack get pods
```

### Rollback

State honestly whether rollback is possible. If it is, set `understack_ref`
back to `v0.4.25` and resync. If it is not, say so plainly and give the
recovery path instead.

### Known issues

- None.
````
7 changes: 7 additions & 0 deletions docs/operator-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ clouds:
In the above case `uc-prod-infra` would be the operator area while `uc-prod` would
be the regular project area.

## Upgrading

- [Release Notes](../release-notes/index.md) - What you have to do to move a
deployment between versions. If you deploy from `main`, read the
[Unreleased section](../release-notes/v0.4.md#unreleased) of the current
series.

## Infrastructure Topics

- [Gateway API Migration Guide](gateway-api.md) - Migration from ingress-nginx to Kubernetes Gateway API with Envoy Gateway
Expand Down
54 changes: 54 additions & 0 deletions docs/release-notes/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Release Notes

These pages document **what an operator has to do** to move a deployment from
one version of UnderStack to another: required changes to your deployment
repository, new or removed secrets, one-time manual steps, and how to roll
back.

They are deliberately **not** a changelog. For the full list of merged pull
requests in a given tag, see the
[GitHub releases page](https://github.com/rackerlabs/understack/releases).

!!! tip "If you deploy from `main`"
The default deployment model sets `understack_ref: HEAD`, so most
deployments track `main` continuously rather than a tag. Read the
[Unreleased section](v0.4.md#unreleased) of the current series page. It
covers everything merged to `main` since the most recent tag, and it is
updated in the same pull request as the change it describes.

## Series

| Series | Status |
| ------ | ------ |
| [v0.4.x](v0.4.md) | Current |

## How to read these pages

- There is one page per **minor** series. Each page lists versions
newest-first.
- **Only versions that require operator action get a section.** Most do not, so
most versions are absent from these pages. If a version has no section, it
needed nothing beyond a normal resync.
- Each section states its **Impact** (`Action required` or `Informational`) and
which cluster types it **applies to**.

## Pinning a version

To upgrade deliberately rather than continuously, pin `understack_ref` to a tag
in your cluster values file instead of leaving it at `HEAD`. See
[ArgoCD Application Management](../operator-guide/argocd-helm-chart.md) for the
full explanation of how refs are resolved.

```yaml title="$CLUSTER_NAME/deploy.yaml"
understack_ref: v0.4.26
```

## Related upgrade guides

Some upgrades are large enough to warrant a standalone guide. Release notes link
to these rather than duplicating them.

- [Gateway API Migration Guide](../operator-guide/gateway-api.md) — moving from
ingress-nginx to Envoy Gateway.
- [MariaDB Operator Upgrade Runbook](../operator-guide/mariadb-upgrade-runbook.md)
— upgrading the operator, with backup and restore steps.
Loading
Loading