| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" |
| 5 | tmp_dir="$(mktemp -d)" |
| 6 | trap 'rm -rf "${tmp_dir}"' EXIT |
| 7 | |
| 8 | make_fixture() { |
| 9 | local root="$1" |
| 10 | mkdir -p \ |
| 11 | "${root}/bin" \ |
| 12 | "${root}/crates/example" \ |
| 13 | "${root}/crates/tui" \ |
| 14 | "${root}/docs" \ |
| 15 | "${root}/extensions/vscode" \ |
| 16 | "${root}/npm/codewhale" \ |
| 17 | "${root}/npm/runtime-sdk" \ |
| 18 | "${root}/scripts/remote-smoke" \ |
| 19 | "${root}/scripts/release" \ |
| 20 | "${root}/web/lib" \ |
| 21 | "${root}/web/scripts" |
| 22 | |
| 23 | cp "${repo_root}/scripts/release/prepare-release.sh" \ |
| 24 | "${root}/scripts/release/prepare-release.sh" |
| 25 | |
| 26 | cat >"${root}/Cargo.toml" <<'EOF' |
| 27 | [workspace] |
| 28 | members = ["crates/example"] |
| 29 | |
| 30 | [workspace.package] |
| 31 | version = "0.8.68" |
| 32 | EOF |
| 33 | |
| 34 | cat >"${root}/crates/example/Cargo.toml" <<'EOF' |
| 35 | [package] |
| 36 | name = "codewhale-example" |
| 37 | version.workspace = true |
| 38 | |
| 39 | [dependencies] |
| 40 | codewhale-core = { path = "../core", version = "0.8.68" } |
| 41 | EOF |
| 42 | |
| 43 | cat >"${root}/npm/codewhale/package.json" <<'EOF' |
| 44 | { |
| 45 | "name": "codewhale", |
| 46 | "version": "0.8.68", |
| 47 | "codewhaleBinaryVersion": "0.8.68" |
| 48 | } |
| 49 | EOF |
| 50 | |
| 51 | cat >"${root}/npm/runtime-sdk/package.json" <<'EOF' |
| 52 | { |
| 53 | "name": "@codewhale/runtime-sdk", |
| 54 | "version": "0.8.68" |
| 55 | } |
| 56 | EOF |
| 57 | |
| 58 | cat >"${root}/extensions/vscode/package.json" <<'EOF' |
| 59 | { |
| 60 | "name": "codewhale-vscode", |
| 61 | "version": "0.8.68" |
| 62 | } |
| 63 | EOF |
| 64 | |
| 65 | cat >"${root}/extensions/vscode/package-lock.json" <<'EOF' |
| 66 | { |
| 67 | "name": "codewhale-vscode", |
| 68 | "version": "0.8.68", |
| 69 | "lockfileVersion": 3, |
| 70 | "packages": { |
| 71 | "": { |
| 72 | "name": "codewhale-vscode", |
| 73 | "version": "0.8.68" |
| 74 | } |
| 75 | } |
| 76 | } |
| 77 | EOF |
| 78 | |
| 79 | cat >"${root}/package-lock.json" <<'EOF' |
| 80 | { |
| 81 | "name": "fixture", |
| 82 | "lockfileVersion": 3, |
| 83 | "packages": { |
| 84 | "": { "name": "fixture" }, |
| 85 | "npm/codewhale": { |
| 86 | "version": "0.8.68", |
| 87 | "license": "MIT" |
| 88 | }, |
| 89 | "npm/runtime-sdk": { |
| 90 | "version": "0.8.68", |
| 91 | "license": "MIT" |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | EOF |
| 96 | |
| 97 | cat >"${root}/Cargo.lock" <<'EOF' |
| 98 | # fixture lock |
| 99 | EOF |
| 100 | |
| 101 | cat >"${root}/CHANGELOG.md" <<'EOF' |
| 102 | ## [Unreleased] |
| 103 | |
| 104 | ## [0.9.0] - 2026-07-15 |
| 105 | |
| 106 | ### Changed |
| 107 | |
| 108 | - Test release. |
| 109 | EOF |
| 110 | |
| 111 | cat >"${root}/docs/INSTALL.md" <<'EOF' |
| 112 | The npm wrapper installs the matching published binaries. |
| 113 | |
| 114 | codewhale --version # prints the published version that was installed |
| 115 | EOF |
| 116 | |
| 117 | cat >"${root}/docs/public-surface-facts.json" <<'EOF' |
| 118 | { |
| 119 | "sourceCandidate": { "version": "0.8.68" }, |
| 120 | "latestPublishedRelease": { |
| 121 | "tag": "v0.8.67", |
| 122 | "version": "0.8.67", |
| 123 | "url": "https://example.invalid/v0.8.67" |
| 124 | }, |
| 125 | "screenshot": { "sourceVersion": "0.8.67" } |
| 126 | } |
| 127 | EOF |
| 128 | |
| 129 | cat >"${root}/scripts/remote-smoke/setup-vm.sh" <<'EOF' |
| 130 | #!/usr/bin/env bash |
| 131 | RELEASE_TAG="${RELEASE_TAG:-v0.8.68}" |
| 132 | EOF |
| 133 | |
| 134 | printf 'fixture packaged changelog\n' >"${root}/crates/tui/CHANGELOG.md" |
| 135 | printf 'fixture generated facts\n' >"${root}/web/lib/facts.generated.ts" |
| 136 | |
| 137 | for readme in README.md README.zh-CN.md README.ja-JP.md README.vi.md README.ko-KR.md; do |
| 138 | printf 'Install Codewhale from the package manager.\n' >"${root}/${readme}" |
| 139 | done |
| 140 | |
| 141 | cat >"${root}/bin/cargo" <<'EOF' |
| 142 | #!/usr/bin/env bash |
| 143 | set -euo pipefail |
| 144 | : >"${PREPARE_RELEASE_TEST_MARKERS}/cargo" |
| 145 | EOF |
| 146 | |
| 147 | cat >"${root}/scripts/sync-changelog.sh" <<'EOF' |
| 148 | #!/usr/bin/env bash |
| 149 | set -euo pipefail |
| 150 | : >"${PREPARE_RELEASE_TEST_MARKERS}/sync-changelog" |
| 151 | EOF |
| 152 | |
| 153 | cat >"${root}/scripts/release/check-versions.sh" <<'EOF' |
| 154 | #!/usr/bin/env bash |
| 155 | set -euo pipefail |
| 156 | : >"${PREPARE_RELEASE_TEST_MARKERS}/check-versions" |
| 157 | EOF |
| 158 | |
| 159 | cat >"${root}/scripts/release/check-ohos-deps.sh" <<'EOF' |
| 160 | #!/usr/bin/env bash |
| 161 | set -euo pipefail |
| 162 | : >"${PREPARE_RELEASE_TEST_MARKERS}/check-ohos-deps" |
| 163 | EOF |
| 164 | |
| 165 | cat >"${root}/web/scripts/derive-facts.mjs" <<'EOF' |
| 166 | import { writeFileSync } from "node:fs"; |
| 167 | |
| 168 | writeFileSync(`${process.env.PREPARE_RELEASE_TEST_MARKERS}/derive-facts`, ""); |
| 169 | EOF |
| 170 | |
| 171 | cat >"${root}/web/scripts/derive-changelog.mjs" <<'EOF' |
| 172 | import { writeFileSync } from "node:fs"; |
| 173 | |
| 174 | writeFileSync(`${process.env.PREPARE_RELEASE_TEST_MARKERS}/derive-changelog`, ""); |
| 175 | EOF |
| 176 | |
| 177 | chmod +x \ |
| 178 | "${root}/bin/cargo" \ |
| 179 | "${root}/scripts/release/prepare-release.sh" \ |
| 180 | "${root}/scripts/release/check-ohos-deps.sh" \ |
| 181 | "${root}/scripts/release/check-versions.sh" \ |
| 182 | "${root}/scripts/sync-changelog.sh" |
| 183 | } |
| 184 | |
| 185 | success_root="${tmp_dir}/success" |
| 186 | success_markers="${tmp_dir}/success-markers" |
| 187 | make_fixture "${success_root}" |
| 188 | mkdir -p "${success_markers}" |
| 189 | printf 'Install from a release tag: --tag v0.8.68\n' >"${success_root}/README.md" |
| 190 | |
| 191 | PREPARE_RELEASE_TEST_MARKERS="${success_markers}" \ |
| 192 | PATH="${success_root}/bin:${PATH}" \ |
| 193 | "${success_root}/scripts/release/prepare-release.sh" 0.9.0 >/dev/null |
| 194 | |
| 195 | grep -Fq 'version = "0.9.0"' "${success_root}/Cargo.toml" |
| 196 | grep -Fq 'version = "0.9.0"' "${success_root}/crates/example/Cargo.toml" |
| 197 | grep -Fq '"version": "0.9.0"' "${success_root}/npm/codewhale/package.json" |
| 198 | grep -Fq '"codewhaleBinaryVersion": "0.9.0"' \ |
| 199 | "${success_root}/npm/codewhale/package.json" |
| 200 | grep -Fq '"version": "0.9.0"' "${success_root}/npm/runtime-sdk/package.json" |
| 201 | grep -Fq '"version": "0.9.0"' "${success_root}/extensions/vscode/package.json" |
| 202 | [[ "$(grep -Fc '"version": "0.9.0"' "${success_root}/extensions/vscode/package-lock.json")" == "2" ]] |
| 203 | [[ "$(grep -Fc '"version": "0.9.0"' "${success_root}/package-lock.json")" == "2" ]] |
| 204 | grep -Fq 'RELEASE_TAG="${RELEASE_TAG:-v0.9.0}"' \ |
| 205 | "${success_root}/scripts/remote-smoke/setup-vm.sh" |
| 206 | grep -Fq '"sourceCandidate": { "version": "0.9.0" }' \ |
| 207 | "${success_root}/docs/public-surface-facts.json" |
| 208 | grep -Fq '"tag": "v0.8.67"' \ |
| 209 | "${success_root}/docs/public-surface-facts.json" |
| 210 | grep -Fq '"sourceVersion": "0.8.67"' \ |
| 211 | "${success_root}/docs/public-surface-facts.json" |
| 212 | grep -Fq 'prints the published version that was installed' \ |
| 213 | "${success_root}/docs/INSTALL.md" |
| 214 | grep -Fq -- '--tag v0.9.0' "${success_root}/README.md" |
| 215 | if grep -R -E -- '--tag v[0-9]+\.[0-9]+\.[0-9]+' \ |
| 216 | "${success_root}/README.zh-CN.md" \ |
| 217 | "${success_root}/README.ja-JP.md" \ |
| 218 | "${success_root}/README.vi.md" \ |
| 219 | "${success_root}/README.ko-KR.md"; then |
| 220 | echo "tag-free localized README unexpectedly gained a release tag" >&2 |
| 221 | exit 1 |
| 222 | fi |
| 223 | for marker in cargo sync-changelog derive-facts derive-changelog check-versions check-ohos-deps; do |
| 224 | [[ -f "${success_markers}/${marker}" ]] || { |
| 225 | echo "prepare-release did not reach ${marker}" >&2 |
| 226 | exit 1 |
| 227 | } |
| 228 | done |
| 229 | |
| 230 | same_root="${tmp_dir}/same" |
| 231 | same_markers="${tmp_dir}/same-markers" |
| 232 | same_log="${tmp_dir}/same.log" |
| 233 | make_fixture "${same_root}" |
| 234 | mkdir -p "${same_markers}" |
| 235 | cat >"${same_root}/CHANGELOG.md" <<'EOF' |
| 236 | ## [Unreleased] |
| 237 | |
| 238 | ## [0.8.68] - 2026-07-18 |
| 239 | |
| 240 | ### Changed |
| 241 | |
| 242 | - Test already-prepared release. |
| 243 | EOF |
| 244 | |
| 245 | PREPARE_RELEASE_TEST_MARKERS="${same_markers}" \ |
| 246 | PATH="${same_root}/bin:${PATH}" \ |
| 247 | "${same_root}/scripts/release/prepare-release.sh" 0.8.68 \ |
| 248 | >"${same_log}" |
| 249 | |
| 250 | grep -Fq \ |
| 251 | 'Workspace is already at 0.8.68; refreshing generated release state and rerunning gates.' \ |
| 252 | "${same_log}" |
| 253 | for marker in sync-changelog derive-facts derive-changelog check-versions check-ohos-deps; do |
| 254 | [[ -f "${same_markers}/${marker}" ]] || { |
| 255 | echo "same-version prepare-release did not reach ${marker}" >&2 |
| 256 | exit 1 |
| 257 | } |
| 258 | done |
| 259 | if [[ -f "${same_markers}/cargo" ]]; then |
| 260 | echo "same-version prepare-release unexpectedly mutated Cargo.lock" >&2 |
| 261 | exit 1 |
| 262 | fi |
| 263 | grep -Fq 'version = "0.8.68"' "${same_root}/Cargo.toml" |
| 264 | grep -Fq 'version = "0.8.68"' "${same_root}/crates/example/Cargo.toml" |
| 265 | grep -Fq '"version": "0.8.68"' "${same_root}/npm/codewhale/package.json" |
| 266 | |
| 267 | same_stale_root="${tmp_dir}/same-stale" |
| 268 | same_stale_markers="${tmp_dir}/same-stale-markers" |
| 269 | same_stale_log="${tmp_dir}/same-stale.log" |
| 270 | make_fixture "${same_stale_root}" |
| 271 | mkdir -p "${same_stale_markers}" |
| 272 | python3 - "${same_stale_root}/docs/public-surface-facts.json" <<'PY' |
| 273 | import pathlib, sys |
| 274 | |
| 275 | path = pathlib.Path(sys.argv[1]) |
| 276 | path.write_text(path.read_text().replace( |
| 277 | '"sourceCandidate": { "version": "0.8.68" }', |
| 278 | '"sourceCandidate": { "version": "0.8.67" }', |
| 279 | )) |
| 280 | PY |
| 281 | |
| 282 | if PREPARE_RELEASE_TEST_MARKERS="${same_stale_markers}" \ |
| 283 | PATH="${same_stale_root}/bin:${PATH}" \ |
| 284 | "${same_stale_root}/scripts/release/prepare-release.sh" 0.8.68 \ |
| 285 | >"${same_stale_log}" 2>&1; then |
| 286 | echo "same-version stale source candidate unexpectedly passed" >&2 |
| 287 | exit 1 |
| 288 | fi |
| 289 | grep -Fq \ |
| 290 | "sourceCandidate.version is '0.8.67'; expected '0.8.68'" \ |
| 291 | "${same_stale_log}" |
| 292 | if find "${same_stale_markers}" -type f -print -quit | grep -q .; then |
| 293 | echo "same-version stale source candidate reached downstream release steps" >&2 |
| 294 | exit 1 |
| 295 | fi |
| 296 | |
| 297 | legacy_root="${tmp_dir}/legacy" |
| 298 | legacy_markers="${tmp_dir}/legacy-markers" |
| 299 | make_fixture "${legacy_root}" |
| 300 | mkdir -p "${legacy_markers}" |
| 301 | cat >"${legacy_root}/docs/INSTALL.md" <<'EOF' |
| 302 | The npm wrapper is published at v0.8.68. |
| 303 | codewhale --version # 0.8.68 |
| 304 | EOF |
| 305 | |
| 306 | PREPARE_RELEASE_TEST_MARKERS="${legacy_markers}" \ |
| 307 | PATH="${legacy_root}/bin:${PATH}" \ |
| 308 | "${legacy_root}/scripts/release/prepare-release.sh" 0.9.0 >/dev/null |
| 309 | |
| 310 | grep -Fq 'wrapper is published at v0.9.0' "${legacy_root}/docs/INSTALL.md" |
| 311 | grep -Fq 'codewhale --version # 0.9.0' "${legacy_root}/docs/INSTALL.md" |
| 312 | |
| 313 | stale_root="${tmp_dir}/stale" |
| 314 | stale_markers="${tmp_dir}/stale-markers" |
| 315 | stale_log="${tmp_dir}/stale.log" |
| 316 | make_fixture "${stale_root}" |
| 317 | mkdir -p "${stale_markers}" |
| 318 | printf 'Stale install example: --tag v0.8.67\n' >"${stale_root}/README.ja-JP.md" |
| 319 | |
| 320 | if PREPARE_RELEASE_TEST_MARKERS="${stale_markers}" \ |
| 321 | PATH="${stale_root}/bin:${PATH}" \ |
| 322 | "${stale_root}/scripts/release/prepare-release.sh" 0.9.0 \ |
| 323 | >"${stale_log}" 2>&1; then |
| 324 | echo "stale README release tag unexpectedly passed" >&2 |
| 325 | exit 1 |
| 326 | fi |
| 327 | |
| 328 | grep -Fq \ |
| 329 | 'README.ja-JP.md has release tag version(s) 0.8.67; expected 0.8.68' \ |
| 330 | "${stale_log}" |
| 331 | grep -Fq 'version = "0.8.68"' "${stale_root}/Cargo.toml" |
| 332 | grep -Fq 'version = "0.8.68"' "${stale_root}/crates/example/Cargo.toml" |
| 333 | grep -Fq '"version": "0.8.68"' "${stale_root}/npm/codewhale/package.json" |
| 334 | if find "${stale_markers}" -type f -print -quit | grep -q .; then |
| 335 | echo "stale README validation mutated downstream release state" >&2 |
| 336 | exit 1 |
| 337 | fi |
| 338 | |
| 339 | # A downstream command failure must restore every release-bearing file, not |
| 340 | # strand the fixture after the text mutation phase. |
| 341 | failure_root="${tmp_dir}/failure" |
| 342 | failure_snapshot="${tmp_dir}/failure-before" |
| 343 | failure_markers="${tmp_dir}/failure-markers" |
| 344 | failure_log="${tmp_dir}/failure.log" |
| 345 | make_fixture "${failure_root}" |
| 346 | mkdir -p "${failure_markers}" |
| 347 | cp -R "${failure_root}" "${failure_snapshot}" |
| 348 | cat >"${failure_root}/bin/cargo" <<'EOF' |
| 349 | #!/usr/bin/env bash |
| 350 | set -euo pipefail |
| 351 | exit 42 |
| 352 | EOF |
| 353 | chmod +x "${failure_root}/bin/cargo" |
| 354 | # Snapshot the intentional failing cargo shim too; only release preparation's |
| 355 | # effects should be absent after rollback. |
| 356 | cp "${failure_root}/bin/cargo" "${failure_snapshot}/bin/cargo" |
| 357 | |
| 358 | if PREPARE_RELEASE_TEST_MARKERS="${failure_markers}" \ |
| 359 | PATH="${failure_root}/bin:${PATH}" \ |
| 360 | "${failure_root}/scripts/release/prepare-release.sh" 0.9.0 \ |
| 361 | >"${failure_log}" 2>&1; then |
| 362 | echo "failing cargo refresh unexpectedly passed" >&2 |
| 363 | exit 1 |
| 364 | fi |
| 365 | |
| 366 | grep -Fq "restored the checkout's pre-run release files" "${failure_log}" |
| 367 | diff -ru "${failure_snapshot}" "${failure_root}" |
| 368 | |
| 369 | echo "prepare-release tests passed" |
| 370 |