Repository navigation
Expand file tree
/
Copy pathrelease-lint.sh
More file actions
executable file
·143 lines (129 loc) · 5.73 KB
/
Copy pathrelease-lint.sh
File metadata and controls
executable file
·143 lines (129 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#!/usr/bin/env bash
# Release-readiness lint: verifies the version chores are complete before a
# release can publish. Single source of truth for the checks shared by CI
# (`release-readiness` job in ci.yml, on every PR) and the Release workflow
# (`version` job in release.yml, before anything builds or publishes).
#
# Checks (all failures are collected and reported together):
# 1. The version is a release version — X.Y.Z, or X.Y.Z-rc.N (main carries
# the newest cut rc between release-sync merges; see
# docs/release-train/DESIGN.md §3.7) — and matches Cargo.toml.
# With --stable-only an rc version is refused.
# 2. Version coherence: the offline stamp (`scripts/release.py stamp
# <version> --check`, what `scripts/version-sync.sh` runs) is a no-op —
# every stamped site (Cargo.toml, Cargo.lock, npm manifests and lock)
# already carries the version, byte for byte. Catches hand-edited drift
# in any single site. Plus `scripts/release.py npm-lock-check`: the npm
# wrapper's package-lock.json agrees with its package.json beyond the
# versions (dependency maps, engines, bin, a node_modules/ entry per
# dependency), the drift the old networked lock refresh used to catch.
# Offline; writes nothing.
# 3. CHANGELOG.md has a `## [<version>]` section with non-empty release
# notes, and a stable version has no leftover `[<version>-rc.N]`
# sections (they fold into it at promotion). Skipped with --sync-only.
# 4. With --tag-check: the tag v<version> does not already exist at a
# commit other than HEAD (existing at HEAD is allowed — that is a re-run
# of a release that already tagged; mirrors the Release workflow
# semantics). With --tag-exists: the tag v<version> must already exist
# on origin (the release-sync PR only ever moves main to a cut tag).
#
# Usage: release-lint.sh [--sync-only] [--stable-only] [--tag-check|--tag-exists] [<version>]
# <version> defaults to the workspace version in Cargo.toml.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"
SYNC_ONLY=false
STABLE_ONLY=false
TAG_CHECK=false
TAG_EXISTS=false
VERSION=""
for arg in "$@"; do
case "$arg" in
--sync-only) SYNC_ONLY=true ;;
--stable-only) STABLE_ONLY=true ;;
--tag-check) TAG_CHECK=true ;;
--tag-exists) TAG_EXISTS=true ;;
-*)
echo "release-lint: unknown flag: $arg" >&2
exit 2
;;
*) VERSION="$arg" ;;
esac
done
FAILED=0
fail() {
FAILED=1
# ::error:: annotates the run + PR when under GitHub Actions.
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
echo "::error title=release-lint::$*"
else
echo "release-lint: error: $*" >&2
fi
}
note() {
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
echo "::notice title=release-lint::$*"
else
echo "release-lint: $*"
fi
}
# ── 1. version shape + Cargo.toml agreement ─────────────────────────────────
CARGO_VERSION="$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
if [ -z "$VERSION" ]; then
VERSION="$CARGO_VERSION"
fi
KIND=any
if [ "$STABLE_ONLY" = "true" ]; then
KIND=stable
fi
if ! SEMVER_ERR="$(python3 scripts/release.py semver validate --kind "$KIND" "$VERSION" 2>&1 >/dev/null)"; then
fail "${SEMVER_ERR#release.py: error: }"
fi
if [ "$VERSION" != "$CARGO_VERSION" ]; then
fail "requested version $VERSION != Cargo.toml workspace version $CARGO_VERSION (run scripts/version-sync.sh $VERSION)"
fi
# ── 2. version coherence: the stamp must be a no-op ─────────────────────────
if STAMP_ERR="$(python3 scripts/release.py stamp --check "$VERSION" 2>&1 >/dev/null)"; then
note "version coherence OK: every stamped site already carries $VERSION"
else
fail "${STAMP_ERR#release.py: error: } (run scripts/version-sync.sh $VERSION)"
fi
if LOCK_ERR="$(python3 scripts/release.py npm-lock-check 2>&1 >/dev/null)"; then
note "npm lock OK: npm/socket-patch/package-lock.json matches package.json"
else
fail "$(printf '%s' "$LOCK_ERR" | tr '\n' ';')"
fi
# ── 3. CHANGELOG section + non-empty notes ─────────────────────────────────
if [ "$SYNC_ONLY" = "false" ]; then
if CHANGELOG_MSG="$(python3 scripts/release.py changelog check --version "$VERSION" 2>&1)"; then
note "CHANGELOG OK: $CHANGELOG_MSG"
else
fail "${CHANGELOG_MSG#release.py: error: } — cut it with scripts/release.py changelog cut --version $VERSION --date <YYYY-MM-DD> (or write the section by hand)"
fi
fi
# ── 4. tag collision (opt-in: needs the remote) ─────────────────────────────
if [ "$TAG_EXISTS" = "true" ]; then
if [ -n "$(git ls-remote origin "refs/tags/v${VERSION}")" ]; then
note "tag v${VERSION} exists"
else
fail "tag v${VERSION} does not exist — main's version may only move to a version the release train already cut"
fi
fi
if [ "$TAG_CHECK" = "true" ]; then
# HEAD is the release commit in the Release workflow (GITHUB_SHA) and the
# PR merge commit in CI; in both cases an existing tag at any OTHER commit
# means this version was already released from different code.
EXISTING_SHA="$(git ls-remote origin "refs/tags/v${VERSION}" | cut -f1)"
HEAD_SHA="$(git rev-parse HEAD)"
if [ -z "$EXISTING_SHA" ]; then
note "tag v${VERSION} does not exist yet"
elif [ "$EXISTING_SHA" = "$HEAD_SHA" ]; then
note "tag v${VERSION} already points at HEAD — a retry of a previous release run"
else
fail "tag v${VERSION} already exists at ${EXISTING_SHA} (HEAD is ${HEAD_SHA}) — bump to a new version"
fi
fi
if [ "$FAILED" -ne 0 ]; then
exit 1
fi
note "all checks passed for $VERSION"