diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..4726f755b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ + + +## 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. diff --git a/.github/workflows/release-note-check.yaml b/.github/workflows/release-note-check.yaml new file mode 100644 index 000000000..ac9117de4 --- /dev/null +++ b/.github/workflows/release-note-check.yaml @@ -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 diff --git a/.markdownlint.yml b/.markdownlint.yml index 5a3d241e2..6ea1054ab 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -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 diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 000000000..5d2c4238d --- /dev/null +++ b/RELEASING.md @@ -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:** `` in namespace ``, keys ``, ``. +- **Removed:** ``, safe to delete after upgrading. + +### Chart and image versions + +- OpenStack-Helm `neutron` chart: `` to `` + (`charts/argocd-understack/values.yaml`). +- `keystone` image series: `` to `` + (`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. +```` diff --git a/docs/operator-guide/index.md b/docs/operator-guide/index.md index 1bac29615..48d23dc78 100644 --- a/docs/operator-guide/index.md +++ b/docs/operator-guide/index.md @@ -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 diff --git a/docs/release-notes/index.md b/docs/release-notes/index.md new file mode 100644 index 000000000..b1c8c0c96 --- /dev/null +++ b/docs/release-notes/index.md @@ -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. diff --git a/docs/release-notes/v0.4.md b/docs/release-notes/v0.4.md new file mode 100644 index 000000000..1510d9c88 --- /dev/null +++ b/docs/release-notes/v0.4.md @@ -0,0 +1,48 @@ + +# Release Notes: v0.4.x + +Operator-facing upgrade notes for the v0.4 series, newest first. See the +[Release Notes index](index.md) for how to read these pages. + +## Unreleased + +Changes merged to `main` but not yet in a tagged release. If you deploy with +`understack_ref: HEAD`, this section applies to you now. + +### Action required + +- _Nothing yet._ + +### Deploy repo changes + +- _Nothing yet._ + +### Deprecations and removals + +- _Nothing yet._ + +### Notes + +- _Nothing yet._ + +## v0.4.25 and earlier + +Per-version upgrade notes begin with **v0.4.26**. For earlier versions the only +record is the auto-generated pull request list on the +[GitHub releases page](https://github.com/rackerlabs/understack/releases). + +We have deliberately not reconstructed per-version notes for those releases: +inferring operator impact from pull request titles after the fact produces +confident-sounding instructions nobody has verified. The operator-affecting +changes from before v0.4.26 that _are_ documented have their own guides: + +- [Gateway API Migration Guide](../operator-guide/gateway-api.md) — the move + from ingress-nginx to Envoy Gateway. Required if your deployment still runs + ingress-nginx. +- [MariaDB Operator Upgrade Runbook](../operator-guide/mariadb-upgrade-runbook.md) + — upgrading the MariaDB operator, including backup and restore. diff --git a/properdocs.yml b/properdocs.yml index 1cc2c637a..6c8a27329 100644 --- a/properdocs.yml +++ b/properdocs.yml @@ -262,6 +262,13 @@ nav: - 'Scripts and Tools': - operator-guide/scripts.md - operator-guide/understackctl.md + # Keep this list newest-series-first and maintain it by hand. Do not switch it + # to include_dir_to_nav: that plugin only does an ascending ASCII sort, so + # v0.10.md would sort before v0.9.md, and its reverse toggle is global and + # would also flip the generated Workflows section. + - 'Release Notes': + - release-notes/index.md + - release-notes/v0.4.md - 'User Guide': - user-guide/index.md - user-guide/openstack-cli.md