Skip to content

Commit b02d78f

Browse files
committed
Merge pull request 'ci(release): enforce artifact-consistent versions' (#4608) from harden-release-artifact-version-consistency into staging
Reviewed-on: https://code.carverauto.dev/carverauto/serviceradar/pulls/4608
2 parents 8ae9d5c + a1e57c1 commit b02d78f

12 files changed

Lines changed: 707 additions & 52 deletions

File tree

.forgejo/workflows/release.yml

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ on:
2121
workflow_dispatch:
2222
inputs:
2323
tag:
24-
description: "Release tag to publish (defaults to the event tag or v-prefixed VERSION)."
25-
required: false
24+
description: "Existing release tag to retry (for example, v1.4.22)."
25+
required: true
2626
type: string
2727
dry_run:
2828
description: "Run publish_packages in --dry_run mode."
@@ -127,31 +127,34 @@ jobs:
127127
128128
git fetch --no-tags origin +refs/heads/staging:refs/remotes/origin/staging
129129
130-
tag="${INPUT_TAG}"
131-
if [[ -z "${tag}" && "${GITHUB_REF}" == refs/tags/* ]]; then
130+
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
131+
tag="${INPUT_TAG}"
132+
if [[ -z "${tag}" ]]; then
133+
echo "Manual release dispatch requires an existing release tag." >&2
134+
exit 1
135+
fi
136+
./scripts/validate-release-tag.sh "${tag}"
137+
git fetch --no-tags origin "refs/tags/${tag}:refs/tags/${tag}"
138+
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
132139
tag="${GITHUB_REF#refs/tags/}"
140+
./scripts/validate-release-tag.sh "${tag}"
141+
else
142+
echo "Release publication requires a tag-push event or an explicit manual tag." >&2
143+
exit 1
133144
fi
134-
if [[ -z "${tag}" ]]; then
135-
version="$(tr -d '\r\n' < VERSION)"
136-
tag="v${version}"
137-
fi
138-
./scripts/validate-release-tag.sh "${tag}"
139145
140-
if git rev-parse -q --verify "refs/tags/${tag}^{commit}" >/dev/null; then
141-
release_commit="$(git rev-list -n1 "refs/tags/${tag}^{commit}")"
142-
else
143-
if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then
144-
echo "Release tag ${tag} is not available in this checkout" >&2
145-
exit 1
146-
fi
147-
release_commit="$(git rev-parse HEAD)"
146+
if ! git rev-parse -q --verify "refs/tags/${tag}^{commit}" >/dev/null; then
147+
echo "Release tag ${tag} is not available in this checkout." >&2
148+
exit 1
148149
fi
150+
release_commit="$(git rev-list -n1 "refs/tags/${tag}^{commit}")"
149151
150152
if ! git merge-base --is-ancestor "${release_commit}" "${RELEASE_BASE_REF}"; then
151153
echo "Release commit ${release_commit} is not reachable from ${RELEASE_BASE_REF}" >&2
152154
exit 1
153155
fi
154156
157+
./scripts/validate-release-metadata.sh "${tag}" "${release_commit}"
155158
printf 'tag=%s\ncommit=%s\n' "${tag}" "${release_commit}" >> "${GITHUB_OUTPUT}"
156159
157160
- name: Cache Bazel artifacts
@@ -246,13 +249,8 @@ jobs:
246249
exit 1
247250
fi
248251
252+
./scripts/validate-release-metadata.sh "${tag}" "${release_commit}" >/dev/null
249253
version="${tag#v}"
250-
file_version="$(git show "${release_commit}:VERSION")"
251-
file_version="${file_version%%[$'\r\n']*}"
252-
if [[ -n "$file_version" && "$file_version" != "$version" ]]; then
253-
echo "VERSION file ($file_version) does not match release tag ($version)" >&2
254-
exit 1
255-
fi
256254
257255
# Auto-detect pre-release from version pattern
258256
is_prerelease="${INPUT_PRERELEASE}"
@@ -456,7 +454,11 @@ jobs:
456454
457455
- name: Publish Helm chart to OCI registry
458456
env:
457+
RELEASE_TAG: ${{ steps.release.outputs.tag }}
458+
RELEASE_COMMIT: ${{ steps.release.outputs.commit }}
459459
VERSION: ${{ steps.release.outputs.version }}
460+
CHART_OCI_USERNAME: ${{ secrets.HARBOR_CHART_ROBOT_USERNAME }}
461+
CHART_OCI_TOKEN: ${{ secrets.HARBOR_CHART_ROBOT_SECRET }}
460462
run: |
461463
set -euo pipefail
462464
export HELM_STATE_DIR="${PWD}/.helm-home"
@@ -473,12 +475,15 @@ jobs:
473475
echo "${token}" | ./scripts/run-helm.sh registry login "${OCI_REGISTRY}" -u "${user}" --password-stdin
474476
}
475477
476-
if [[ -z "${OCI_USERNAME:-}" || -z "${OCI_TOKEN:-}" ]]; then
477-
echo "Missing Harbor robot credentials. Set HARBOR_ROBOT_USERNAME and HARBOR_ROBOT_SECRET." >&2
478+
if [[ -z "${CHART_OCI_USERNAME:-}" || -z "${CHART_OCI_TOKEN:-}" ]]; then
479+
echo "Missing protected chart publisher credentials. Configure HARBOR_CHART_ROBOT_USERNAME and HARBOR_CHART_ROBOT_SECRET in the release environment." >&2
478480
exit 1
479481
fi
480482
481-
login_registry "${OCI_USERNAME}" "${OCI_TOKEN}"
483+
login_registry "${CHART_OCI_USERNAME}" "${CHART_OCI_TOKEN}"
484+
485+
./scripts/validate-release-metadata.sh "${RELEASE_TAG}" "${RELEASE_COMMIT}" >/dev/null
486+
./scripts/check-oci-chart-version-available.sh "${VERSION}"
482487
483488
# Package and push chart
484489
./scripts/run-helm.sh package helm/serviceradar
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: Release Artifact Integrity
3+
---
4+
5+
# Release artifact integrity
6+
7+
ServiceRadar product releases use one version identity across every published
8+
artifact. For product version `X.Y.Z`, all of the following must agree:
9+
10+
- Git tag `vX.Y.Z`
11+
- `VERSION` value `X.Y.Z`
12+
- Helm `Chart.yaml` `version` and `appVersion` values `X.Y.Z`
13+
- OCI Helm chart tag `X.Y.Z`
14+
- OCI image semantic tags `vX.Y.Z`
15+
- Forgejo release tag `vX.Y.Z`
16+
17+
The release workflow validates this agreement at the tagged commit. It refuses
18+
to publish from an arbitrary branch head, including during manual dispatch.
19+
20+
## Cut a release
21+
22+
Prepare the changelog entry, then use the supported release helper from a clean
23+
release branch:
24+
25+
```bash
26+
scripts/cut-release.sh --version X.Y.Z --dry-run
27+
scripts/cut-release.sh --version X.Y.Z
28+
```
29+
30+
The helper checks both `origin` for `vX.Y.Z` and the OCI chart repository for
31+
chart version `X.Y.Z` before changing release metadata. Both checks fail closed:
32+
if Git or the registry cannot be queried reliably, stop and restore access
33+
instead of bypassing the guard.
34+
35+
Follow the merge and tag-push commands printed by the helper. The release tag
36+
must point to a commit reachable from `staging`. Do not push a release tag until
37+
the release change has merged.
38+
39+
## Retry a failed publication
40+
41+
Use the release workflow's manual dispatch and provide the existing release tag.
42+
The workflow fetches that exact tag, verifies that it is reachable from
43+
`origin/staging`, and revalidates the metadata at the tagged commit. A branch,
44+
commit SHA, missing tag, or value derived from the current `VERSION` file is not
45+
a valid manual release source.
46+
47+
The retry remains subject to artifact immutability. It can complete missing
48+
artifacts, but it cannot replace an OCI chart version that already exists.
49+
50+
## Recover from an occupied version
51+
52+
Treat a published OCI chart version as an immutable audit record, even if no
53+
matching Git tag or Forgejo release exists. Deleting a Git tag does not make an
54+
OCI chart version reusable, and operators must not delete or overwrite the chart
55+
to force a release through.
56+
57+
If the requested version is occupied:
58+
59+
1. Leave the existing Git and OCI records intact.
60+
2. Select the next unoccupied product version.
61+
3. Add a changelog entry for that version.
62+
4. Run `scripts/cut-release.sh` again with the new version.
63+
64+
Chart-only versions `1.4.16`, `1.4.17`, and `1.4.18` are known historical
65+
records and must remain reserved. The same rule applies to any future partial
66+
or interrupted release.
67+
68+
## Protect chart publication credentials
69+
70+
The protected Forgejo `release` environment must provide these chart-specific
71+
secrets:
72+
73+
- `HARBOR_CHART_ROBOT_USERNAME`
74+
- `HARBOR_CHART_ROBOT_SECRET`
75+
76+
The associated Harbor robot account must have only the permissions needed to
77+
pull and push `serviceradar/charts/serviceradar`. Do not grant delete authority.
78+
Protect the Forgejo environment with release approvals and the repository's
79+
release tag policy.
80+
81+
The general `HARBOR_ROBOT_USERNAME` and `HARBOR_ROBOT_SECRET` credentials used
82+
by image builds and ordinary CI must not have push or delete authority for the
83+
product chart repository. Developer and validation paths may receive read-only
84+
access when they need to check whether a chart version exists.
85+
86+
These Harbor and Forgejo access controls are deployment configuration. Verify
87+
them whenever credentials are rotated or a new runner is introduced; the
88+
workflow's separate secret names prevent accidental reuse but cannot replace
89+
registry-side authorization.

docs/sidebars.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ const sidebars: SidebarsConfig = {
132132
{type: 'doc', id: 'cnpg-monitoring', label: 'CNPG Monitoring'},
133133
{type: 'doc', id: 'observability-rollup-recovery', label: 'Observability Rollup Recovery'},
134134
{type: 'doc', id: 'object-store-retention', label: 'Object Store Retention'},
135+
{type: 'doc', id: 'release-artifact-integrity', label: 'Release Artifact Integrity'},
135136
{type: 'doc', id: 'troubleshooting-guide', label: 'Troubleshooting'},
136137
],
137138
},
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
## Context
2+
3+
ServiceRadar currently uses the same numeric namespace for product Git tags
4+
(`vX.Y.Z`) and OCI Helm chart packages (`X.Y.Z`). The intended release path
5+
updates `VERSION` and `Chart.yaml` together, then publishes from a release
6+
tag. In practice, chart-only packages were published with new chart versions
7+
while `VERSION`, application images, and Git releases remained at `1.4.15`.
8+
Those immutable package versions now block a later product release from using
9+
the same number.
10+
11+
## Goals / Non-Goals
12+
13+
### Goals
14+
15+
- Make a product version explainable and traceable to one immutable source.
16+
- Detect occupied Git and OCI chart versions before a release branch is
17+
mutated or a publish job starts.
18+
- Prevent manual workflow dispatch from publishing product artifacts from an
19+
untagged commit.
20+
- Preserve the ability to release Helm configuration fixes promptly, but make
21+
those releases visible as product releases.
22+
23+
### Non-Goals
24+
25+
- Delete, retag, overwrite, or reinterpret existing OCI packages.
26+
- Rewrite Git history or reuse an already published version.
27+
- Automatically sync demo or production as part of this guard.
28+
- Create a second, independent public chart-versioning scheme in this change.
29+
30+
## Decisions
31+
32+
### A product release has one version identity
33+
34+
For a product release `X.Y.Z`, the Git tag is `vX.Y.Z`, `VERSION` is `X.Y.Z`,
35+
and Helm `version` and `appVersion` are both `X.Y.Z`. The release workflow
36+
publishes the OCI chart `X.Y.Z` and image tags `vX.Y.Z` only after validating
37+
those inputs. A Helm-only correction therefore receives a formal product
38+
release and changelog entry instead of silently reserving a future product
39+
number.
40+
41+
### The tag, not a dispatch input or HEAD, is the source of truth
42+
43+
Push and manual-dispatch paths resolve an already existing tag, check that its
44+
commit is reachable from `origin/staging`, and check all source metadata at
45+
that commit. Manual dispatch is a retry/control surface for a real tag, not a
46+
way to manufacture a release identity from an arbitrary branch or commit.
47+
48+
### Occupancy checks fail closed before mutation and publication
49+
50+
The cut helper queries both the remote Git tag namespace and the OCI chart
51+
repository. An existing chart tag, an existing Git tag, or an inability to
52+
verify either source aborts the cut before any release metadata is written.
53+
The workflow repeats the artifact-occupancy check immediately before chart
54+
publication to close the time-of-check/time-of-use window.
55+
56+
### Publish authority is isolated
57+
58+
Only the protected signing workflow receives write credentials for the OCI
59+
chart repository. Normal developer and CI paths may perform read-only
60+
occupancy checks. The release runbook documents how an operator responds to a
61+
pre-existing version: select a new version, update the changelog, and preserve
62+
the existing artifact as an audit record.
63+
64+
## Risks / Trade-offs
65+
66+
- A configuration-only Helm correction now needs a formal product release.
67+
This is intentional: users can see exactly which ServiceRadar version
68+
introduced the configuration change.
69+
- OCI availability becomes a preflight dependency. The check fails closed,
70+
which may delay a release during a registry outage but avoids an ambiguous
71+
partial publication.
72+
- Existing orphaned package versions remain visible. Removing them would not
73+
reliably invalidate cached/digest-pinned clients and would destroy useful
74+
provenance.
75+
76+
## Migration Plan
77+
78+
1. Treat existing chart-only `1.4.16` through `1.4.18` packages as immutable
79+
historical artifacts and do not reuse their versions.
80+
2. Record that product releases `v1.4.19` through `v1.4.22` were subsequently
81+
published; do not reinterpret or reuse the earlier chart-only versions.
82+
3. Add source, metadata, and occupancy guards with regression tests before the
83+
next product release.
84+
4. Restrict chart write credentials to the protected release environment and
85+
document the operator recovery procedure.
86+
5. Verify a subsequent release and a manual retry both resolve the same tag
87+
and cannot publish a second chart version.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Change: Make product release versions artifact-consistent
2+
3+
## Why
4+
5+
OCI Helm packages `1.4.16`, `1.4.17`, and `1.4.18` were published while the
6+
last corresponding ServiceRadar application release remained `v1.4.15`. Each
7+
package embeds application version and demo image tag `v1.4.15`; it is a
8+
chart-only publication, not a product release. OCI artifact versions are
9+
immutable release identities in practice, so deleting or reusing them would
10+
make caches, locks, provenance, and audit trails disagree. The result is an
11+
unexplained gap in user-visible product versions.
12+
13+
## What Changes
14+
15+
- Make a product release version one immutable identity across its Git tag,
16+
`VERSION`, Helm `version` and `appVersion`, semantic image tags, OCI Helm
17+
package, and Forgejo release.
18+
- Make `scripts/cut-release.sh` fail before modifying files when either the
19+
remote Git tag or the OCI Helm chart version is already occupied or cannot
20+
be verified.
21+
- Require every release-workflow dispatch to resolve an existing release tag
22+
reachable from `staging`; remove the manual-dispatch fallback that treats an
23+
arbitrary checked-out commit as a release source.
24+
- Require the release workflow to verify the tagged commit's version metadata
25+
before it can publish a chart or other product artifacts.
26+
- Reserve chart publishing credentials for the protected release path. A
27+
chart-only configuration change must be shipped in a documented product
28+
release rather than directly consuming a future application version.
29+
- Add release-contract tests and an operator runbook for occupied-version
30+
recovery. Existing chart-only packages remain immutable audit records and
31+
are never deleted or reissued.
32+
33+
## Impact
34+
35+
- Affected specs: new `release-artifact-integrity` capability.
36+
- Affected code: `scripts/cut-release.sh`, release source validation helpers,
37+
`.forgejo/workflows/release.yml`, release-contract tests, Helm release
38+
publishing documentation, and protected registry credential configuration.
39+
- Operational impact: chart-only packages `1.4.16` through `1.4.18` remain
40+
historical audit records. Product releases have since advanced through
41+
`v1.4.22`; future releases cannot bypass the unified release path or reuse an
42+
occupied Git/OCI version.

0 commit comments

Comments
 (0)