Files
DeepSeek-Reasonix/scripts/publish-desktop-github-release.sh
T
SivanColaandffff2004 ac814a5040 fix(release): complete and harden Stable/Preview channels / 补全并加固正式版与预览版渠道 (#6997)
* complete stable and preview channel loop

* fix(site): harden release-channel fallbacks and version scopes

Problem:
Website CLI Stable fallbacks pointed at GitHub releases/latest asset
URLs, incomplete GitHub releases were turned into 404 download links,
and Desktop/CLI Stable races overwrote every .rxv version chip.

Root cause:
CLI releases set make_latest:false so Latest belongs to Desktop Stable;
releaseAssetMap synthesized missing CLI assets; updateStableVersion was
shared across both product surfaces.

Fix:
- CLI fallback always uses the releases page, never /releases/latest.
- Require all 7 CLI assets before accepting a release (match Worker).
- Scope package-manager .rxv updates to CLI Stable only; desktop
  version chips use data-release-version="desktop".

Verification:
node --test site/src/scripts/release-channels.test.mjs
site/src/scripts/download-link.test.mjs

* fix(desktop): unify update channel controls

Problem: The desktop settings page separated channel selection, update status, and install actions across detached rows.

Root cause: The channel picker and updater state machine were rendered as independent controls, so switching channels did not read as one actionable workflow.

Fix: Merge current version/status, Stable and Preview selection, manual refresh, auto-check guidance, and state-specific download/install actions into one compact settings flow. Add the localized channel auto-check hint for English, Simplified Chinese, and Traditional Chinese.

Verification: pnpm --dir desktop/frontend run typecheck; pnpm --dir desktop/frontend run check:css; pnpm --dir desktop/frontend run test:updater (18 passed); pnpm --dir desktop/frontend run build; git diff --cached --check.

* feat(cli): persist release channel selection

Problem:
CLI users had to repeat the verbose --channel flag, and a plain upgrade always returned to Stable instead of following the user's selected public channel.

Root cause:
The native updater had no user-global channel preference, while the website and docs exposed only one-off flag syntax.

Fix:
Add a user-global [cli].update_channel setting, support upgrade stable|preview as persistent switches, retain --channel as a one-off compatibility override, reject conflicting arguments, and synchronize English/Chinese docs plus website commands.

Verification:
- env -u DEEPSEEK_API_KEY GOCACHE=/private/tmp/reasonix-go-cache go test ./...
- env GOCACHE=/private/tmp/reasonix-go-cache go vet ./internal/cli ./internal/config
- npm test (site)
- npm run build (site)
- git diff --check

* fix(release): keep CLI channel pointers monotonic

Problem: Older Stable recovery runs and out-of-order Preview publishers could overwrite a newer public CLI channel pointer.

Root cause: The release workflow wrote cli/<channel>/latest.json unconditionally and had no channel-scoped serialization or strict version comparison.

Fix: Serialize CLI releases by channel, publish immutable per-tag metadata first, validate the current pointer, and move it only when a strict Stable or Preview version tuple is newer.

Verification: bash scripts/release-workflows.test.sh; bash -n scripts/compare-cli-release-tags.sh scripts/release-workflows.test.sh; git diff --check.

* fix(config): preserve symlinked config files

Problem: Persisting the selected CLI update channel could replace a symlinked user config with a regular file.

Root cause: AtomicWriteFile renamed a temporary file over the symlink path instead of the symlink's final target.

Fix: Resolve complete config symlink chains before atomic writes and route both incremental saves and Config.WriteFile through the shared helper.

Verification: go test ./internal/config; focused writable-target and multi-level symlink regression tests; go test ./...; go vet ./internal/cli ./internal/config.

* fix(release): harden stable and preview end-to-end

Problem:
The Stable/Preview loop still allowed stale or cross-channel release pointers, unsafe Desktop release origins and retries, updater events detached from their checked target, and concurrent config writers that could lose user changes.

Root cause:
Release publication lacked one immutable candidate and strict manifest/recovery contracts; Desktop operations were not bound to request, channel, and version; config locks followed logical paths and several writers loaded stale or malformed snapshots.

Fix:
- authorize Desktop candidates from protected main-v2, separate the current control plane from recovered source commits, validate immutable assets, and make GitHub/R2 publication monotonic and resumable
- bind Desktop checks, downloads, verification, and installs to one request/channel/version state machine with strict size and manifest validation
- validate CLI, Desktop, website, and Worker release data consistently across Stable and Preview
- canonicalize and pin config targets, serialize all affected writers, preserve safe symlinks, and fail closed on malformed configuration

Verification:
- go test -race ./...; go vet ./...; go build ./...
- cd desktop && go test -race ./... && go vet ./... && go build ./...
- frontend typecheck, updater tests (31), CSS checks, and production build
- site tests (44) and production build
- Worker typecheck and tests (74)
- release workflow contracts, actionlint, cache guard, shell syntax, diff check, and Linux/Windows cross-builds

Co-authored-by: ffff2004 <[email protected]>

* fix(config): remove obsolete edit wrappers

Problem:
The release-channel hardening left two private config edit wrappers unreachable, causing the pinned golangci-lint unused check to fail.

Root cause:
Callers now resolve and pin final config targets through the transactional edit path, but the superseded convenience wrappers remained in place.

Fix:
Remove only the two unreferenced wrappers while preserving the resolved-target and timeout helpers used by production code and concurrency tests.

Verification:
- go test ./internal/config
- go vet ./...
- golangci-lint v2.12.2 run --timeout=5m
- git diff --check

* fix(release): satisfy workflow shell lint

Problem:
The final CI lint job passed golangci-lint but failed actionlint because the CLI metadata publisher assigned expected_prerelease without reading it.

Root cause:
Prerelease validation moved into validate-cli-release-manifest.sh, leaving duplicate channel-specific shell assignments in the workflow.

Fix:
Remove the unused assignments and keep prerelease validation centralized in the manifest validator.

Verification:
- actionlint v1.7.7 on all release workflows
- bash scripts/release-workflows.test.sh
- bash -n on release helper scripts
- git diff --check

* fix(release): close desktop channel blockers

Problem:
Desktop Preview-to-Stable switches failed for older Debian Stable builds, updater Wails methods broke older one-argument callers, and config edit locks could split across TMPDIR or macOS case aliases.

Root cause:
The privileged helper rejected every downgrade, strict request binding reused legacy exported method names, and lock identity depended on process-local temp directories without Darwin path folding.

Fix:
Allow only signed public Preview-to-Stable Debian downgrades, preserve legacy updater methods beside strict request-bound APIs, and use a stable hardened per-user lock registry with Darwin alias normalization.

Verification:
Root and Desktop tests, Desktop race, Go vet/build, updater 31/31, site 44/44, Worker 74/74, release workflow contracts, cache guard, actionlint, golangci-lint, frontend typecheck/CSS/build, and focused cross-process/security regressions.

---------

Co-authored-by: ffff2004 <[email protected]>
2026-07-29 15:09:26 +08:00

111 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Create or recover one immutable Desktop GitHub release. Existing metadata and
# assets must match the candidate; interrupted runs may only fill missing assets.
set -euo pipefail
if [ "$#" -ne 5 ]; then
echo "usage: $0 TAG VERSION PRERELEASE NOTES_FILE ASSET_DIRECTORY" >&2
exit 2
fi
tag="$1"
version="$2"
prerelease="$3"
notes_file="$4"
asset_dir="${5%/}"
repository="${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
directory_verifier="$script_dir/verify-desktop-release-directory.sh"
title="Reasonix Desktop $version"
case "$prerelease" in
true | false) ;;
*)
echo "Desktop GitHub release prerelease flag must be true or false: $prerelease" >&2
exit 2
;;
esac
if [ ! -s "$notes_file" ]; then
echo "Desktop GitHub release notes are missing or empty: $notes_file" >&2
exit 2
fi
if [ ! -d "$asset_dir" ]; then
echo "Desktop GitHub release asset directory is missing: $asset_dir" >&2
exit 2
fi
if find "$asset_dir" -mindepth 2 -print -quit | grep -q .; then
echo "Desktop GitHub release assets must be top-level files" >&2
exit 2
fi
if ! find "$asset_dir" -mindepth 1 -maxdepth 1 -type f -print -quit | grep -q .; then
echo "Desktop GitHub release asset directory is empty: $asset_dir" >&2
exit 2
fi
work_dir="$(mktemp -d)"
cleanup() {
case "$work_dir" in
*/tmp.* | */reasonix-desktop-release.*) rm -rf -- "$work_dir" ;;
*) echo "refusing to clean unexpected Desktop release temp directory: $work_dir" >&2 ;;
esac
}
trap cleanup EXIT
release_json="$work_dir/release.json"
release_error="$work_dir/release.error"
release_endpoint="repos/${repository}/releases/tags/${tag}"
if ! gh api "$release_endpoint" >"$release_json" 2>"$release_error"; then
if ! grep -Eiq '404|Not Found' "$release_error"; then
cat "$release_error" >&2
exit 1
fi
args=(--title "$title" --notes-file "$notes_file")
if [ "$prerelease" = "true" ]; then
args+=(--prerelease --latest=false)
else
args+=(--latest)
fi
gh release create "$tag" -R "$repository" "${args[@]}"
gh api "$release_endpoint" >"$release_json"
fi
jq -e \
--arg tag "$tag" \
--arg title "$title" \
--rawfile body "$notes_file" \
--argjson prerelease "$prerelease" '
(type == "object") and
(.tag_name == $tag) and
(.name == $title) and
(.body == $body) and
(.draft == false) and
(.prerelease == $prerelease) and
(.assets | type == "array") and
((.assets | map(.name) | length) == (.assets | map(.name) | unique | length))
' "$release_json" >/dev/null
existing_dir="$work_dir/existing"
mkdir -p "$existing_dir"
while IFS= read -r name; do
if [ -z "$name" ] || [ ! -f "$asset_dir/$name" ]; then
echo "Desktop GitHub release has unexpected asset: ${name:-<empty>}" >&2
exit 1
fi
gh release download "$tag" -R "$repository" --pattern "$name" --dir "$existing_dir"
done < <(jq -r '.assets[].name' "$release_json")
bash "$directory_verifier" --allow-missing "$asset_dir" "$existing_dir"
while IFS= read -r -d '' asset; do
name="$(basename "$asset")"
if [ ! -f "$existing_dir/$name" ]; then
gh release upload "$tag" -R "$repository" "$asset"
fi
done < <(find "$asset_dir" -mindepth 1 -maxdepth 1 -type f -print0)
published_dir="$work_dir/published"
mkdir -p "$published_dir"
gh release download "$tag" -R "$repository" --dir "$published_dir"
bash "$directory_verifier" "$asset_dir" "$published_dir"
echo "Desktop GitHub release verified: tag=$tag assets=$(find "$asset_dir" -mindepth 1 -maxdepth 1 -type f | wc -l | tr -d ' ')"