feat: add environment-driven publish scripts for single-branch workflow

Add generic publish pipeline that supports both internal and external
publishing via environment variables, without leaking any internal URLs
or tool references into the public repository.
This commit is contained in:
kite 2026-05-21 17:33:46 +08:00
parent 9a4c966b6d
commit ce9511eed6
5 changed files with 384 additions and 1 deletions

3
.gitignore vendored
View file

@ -9,9 +9,10 @@ dist/
CLAUDE.md
pages/package-lock.json
scripts/hooks/
scripts/publish/.env.internal
CHANGELOG.md
verdor/
vendor/
temp/
# Ignore downloaded Go binary, but keep JS entry point

View file

@ -6,6 +6,7 @@ Makefile
go.mod
go.sum
scripts/hooks/
scripts/publish/
.claude/
.git/
.idea/

View file

@ -0,0 +1,20 @@
# Copy to .env.internal (gitignored) and fill in actual values:
# cp scripts/publish/.env.example scripts/publish/.env.internal
#
# Then publish:
# source scripts/publish/.env.internal && ./scripts/publish/publish.sh
# Override package name
OCR_PKG_NAME=
# npm registry URL
OCR_PUBLISH_REGISTRY=
# Binary download URL template (supports {version} {os} {arch} placeholders)
OCR_URL_PATTERN=
# Checksum file URL template
OCR_CHECKSUM_PATTERN=
# [Optional] Git repo for binary uploads (set to enable upload step)
OCR_RELEASE_GIT_REPO=

64
scripts/publish/_common.sh Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env bash
# _common.sh — Shared utilities for the publish pipeline.
# Sourced by other scripts, not executed directly.
# ── Colors ────────────────────────────────────────────────────────────────────
if [ -t 1 ]; then
RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m'
BLUE='\033[0;34m' BOLD='\033[1m' RESET='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' RESET=''
fi
# ── Logging ───────────────────────────────────────────────────────────────────
info() { echo -e "${BLUE}[INFO]${RESET} $*"; }
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
die() { error "$*"; exit 1; }
# ── Path resolution ──────────────────────────────────────────────────────────
resolve_project_root() {
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
(cd "$script_dir/../.." && pwd)
}
# ── Version helpers ──────────────────────────────────────────────────────────
normalize_version() {
local v="$1"
[[ "$v" != v* ]] && v="v${v}"
echo "$v"
}
npm_version_from_tag() {
echo "${1#v}"
}
# ── Confirmation prompt ──────────────────────────────────────────────────────
confirm() {
local message="${1:-Proceed?}"
if [ "${OCR_FORCE_YES:-0}" = "1" ]; then
return 0
fi
printf "\n%b%s (y/n): ${RESET}" "$BOLD" "$message"
read -r answer
case "$answer" in
[yY]|[yY][eE][sS]) return 0 ;;
*) return 1 ;;
esac
}
# ── Step execution with timing ───────────────────────────────────────────────
run_step() {
local step_name="$1"
local step_cmd="$2"
info "--- ${step_name} ---"
local start_time
start_time=$(date +%s)
eval "$step_cmd" || die "Step '${step_name}' failed."
local elapsed
elapsed=$(( $(date +%s) - start_time ))
success "${step_name} completed (${elapsed}s)"
echo ""
}

297
scripts/publish/publish.sh Executable file
View file

@ -0,0 +1,297 @@
#!/usr/bin/env bash
#
# publish.sh — Environment-driven publish pipeline.
#
# Builds binaries, optionally uploads to a git-based release repo,
# patches package.json with override values, and publishes to npm.
#
# Usage:
# source scripts/publish/.env.internal && ./scripts/publish/publish.sh
#
# Environment variables (all optional — defaults come from package.json):
# OCR_PKG_NAME Override package name
# OCR_PUBLISH_REGISTRY Override npm registry URL
# OCR_URL_PATTERN Override ocrConfig.urlPattern
# OCR_CHECKSUM_PATTERN Override ocrConfig.checksumPattern
# OCR_RELEASE_GIT_REPO Git repo URL for binary uploads (skip if unset)
# OCR_VERSION_OVERRIDE Use this version instead of latest git tag
# OCR_FORCE_YES Skip confirmation prompts (CI mode)
# OCR_SKIP_BUILD Skip build step (use existing dist/)
# OCR_SKIP_UPLOAD Skip git repo upload step
# OCR_SKIP_PUBLISH Skip npm publish step
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_common.sh"
PROJECT_ROOT="$(resolve_project_root)"
cd "$PROJECT_ROOT"
# ── Configuration ─────────────────────────────────────────────────────────────
SKIP_BUILD="${OCR_SKIP_BUILD:-0}"
SKIP_UPLOAD="${OCR_SKIP_UPLOAD:-0}"
SKIP_PUBLISH="${OCR_SKIP_PUBLISH:-0}"
# ── Banner ────────────────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}============================================${RESET}"
echo -e "${BOLD} OpenCodeReview — Publish Pipeline ${RESET}"
echo -e "${BOLD}============================================${RESET}"
echo ""
# ── Resolve version from git tag ─────────────────────────────────────────────
VERSION_TAG="${OCR_VERSION_OVERRIDE:-}"
if [ -z "$VERSION_TAG" ]; then
VERSION_TAG=$(git describe --tags --abbrev=0 2>/dev/null || die "No git tag found. Create one: git tag vX.Y.Z && git push origin --tags")
fi
VERSION_TAG="$(normalize_version "$VERSION_TAG")"
NPM_VERSION="$(npm_version_from_tag "$VERSION_TAG")"
export VERSION_TAG NPM_VERSION
info "Release plan:"
info " Git tag: ${VERSION_TAG}"
info " npm version: ${NPM_VERSION}"
[ -n "${OCR_PKG_NAME:-}" ] && info " Package: ${OCR_PKG_NAME}"
[ -n "${OCR_PUBLISH_REGISTRY:-}" ] && info " Registry: ${OCR_PUBLISH_REGISTRY}"
[ -n "${OCR_RELEASE_GIT_REPO:-}" ] && info " Upload repo: ${OCR_RELEASE_GIT_REPO}"
echo ""
# ── Pre-flight checks ────────────────────────────────────────────────────────
preflight() {
local missing=0
info "Checking prerequisites..."
for cmd in go jq npm git shasum; do
if command -v "$cmd" >/dev/null 2>&1; then
info " ${cmd}: found"
else
error " ${cmd}: NOT FOUND"
missing=1
fi
done
[ "$missing" -eq 0 ] || die "Missing required tools."
success "All tools available"
echo ""
local status
status=$(git status --porcelain 2>/dev/null || true)
if [ -n "$status" ]; then
warn "Working tree has uncommitted changes:"
echo "$status"
die "Please commit or stash changes before publishing."
fi
success "Git working tree is clean"
}
run_step "Pre-flight checks" "preflight"
# ── Confirm ───────────────────────────────────────────────────────────────────
if ! confirm "Ready to publish ${VERSION_TAG}. Continue?"; then
info "Aborted by user."
exit 0
fi
# ── Build ─────────────────────────────────────────────────────────────────────
if [ "$SKIP_BUILD" = "1" ]; then
warn "Skipping build (OCR_SKIP_BUILD=1)"
else
run_step "Building binaries (make dist)" \
"make -C \"$PROJECT_ROOT\" dist"
fi
# ── Sync package.json version ─────────────────────────────────────────────────
sync_version() {
local pkg="$PROJECT_ROOT/package.json"
local current
current=$(jq -r '.version' "$pkg")
if [ "$NPM_VERSION" = "$current" ]; then
info "package.json version already matches ${NPM_VERSION}."
else
info "Updating package.json version: ${current}${NPM_VERSION}"
local tmp
tmp=$(mktemp)
jq --arg v "$NPM_VERSION" '.version = $v' "$pkg" > "$tmp"
mv "$tmp" "$pkg"
success "package.json version updated"
fi
}
run_step "Syncing version" "sync_version"
# ── Upload to git-based release repo (optional) ──────────────────────────────
upload_to_git_repo() {
local repo_url="$OCR_RELEASE_GIT_REPO"
local dist_dir="$PROJECT_ROOT/dist"
local tmp_repo
tmp_repo=$(mktemp -d -t "ocr-release-repo.XXXXXX")
info "Cloning release repo (sparse)..."
git clone --depth 1 --filter=blob:none --sparse "$repo_url" "$tmp_repo"
(cd "$tmp_repo" && git sparse-checkout set --no-cone VERSION)
success "Clone ready"
local bin_dir="${tmp_repo}/bin/${VERSION_TAG}"
mkdir -p "$bin_dir"
local count=0
for f in "$dist_dir"/opencodereview-${VERSION_TAG}-*; do
[ -f "$f" ] || continue
local base target
base=$(basename "$f")
target="${base/opencodereview-${VERSION_TAG}-/opencodereview-}"
info " Copying ${target}"
cp "$f" "$bin_dir/${target}"
count=$((count + 1))
done
if [ "$count" -eq 0 ]; then
rm -rf "$tmp_repo"
die "No binaries found matching opencodereview-${VERSION_TAG}-*"
fi
success "Copied ${count} binaries"
(cd "$bin_dir" && shasum -a 256 opencodereview-* | sort > sha256sum.txt)
success "Checksums generated"
local version_file="${tmp_repo}/VERSION"
if [ -f "$version_file" ]; then
local last
last=$(tail -1 "$version_file" 2>/dev/null || true)
[ "$last" = "$VERSION_TAG" ] || printf "%s\n" "$VERSION_TAG" >> "$version_file"
else
printf "%s\n" "$VERSION_TAG" > "$version_file"
fi
(
cd "$tmp_repo"
git add --sparse -A
if ! git diff --cached --quiet; then
git config user.name "open-code-review-bot"
git config user.email "bot@open-code-review.local"
git commit -m "release: ${VERSION_TAG}"
git push
success "Pushed to release repo"
else
info "No changes to push — binaries already exist."
fi
)
rm -rf "$tmp_repo"
}
if [ "$SKIP_UPLOAD" = "1" ]; then
warn "Skipping upload (OCR_SKIP_UPLOAD=1)"
elif [ -n "${OCR_RELEASE_GIT_REPO:-}" ]; then
run_step "Uploading to release repo" "upload_to_git_repo"
else
info "No OCR_RELEASE_GIT_REPO set, skipping binary upload."
echo ""
fi
# ── Patch package.json for publish ────────────────────────────────────────────
PACKAGE_PATCHED=0
PACKAGE_BACKUP=""
patch_package_json() {
local pkg="$PROJECT_ROOT/package.json"
local changed=0
local tmp
tmp=$(mktemp)
cp "$pkg" "$tmp"
if [ -n "${OCR_PKG_NAME:-}" ]; then
jq --arg n "$OCR_PKG_NAME" '.name = $n' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " name → ${OCR_PKG_NAME}"
changed=1
fi
if [ -n "${OCR_PUBLISH_REGISTRY:-}" ]; then
jq --arg r "$OCR_PUBLISH_REGISTRY" '.publishConfig.registry = $r | .publishConfig.access = "public"' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " publishConfig.registry → ${OCR_PUBLISH_REGISTRY}"
changed=1
fi
if [ -n "${OCR_URL_PATTERN:-}" ]; then
jq --arg u "$OCR_URL_PATTERN" '.ocrConfig.urlPattern = $u' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " ocrConfig.urlPattern → ${OCR_URL_PATTERN}"
changed=1
fi
if [ -n "${OCR_CHECKSUM_PATTERN:-}" ]; then
jq --arg c "$OCR_CHECKSUM_PATTERN" '.ocrConfig.checksumPattern = $c' "$tmp" > "${tmp}.new" && mv "${tmp}.new" "$tmp"
info " ocrConfig.checksumPattern → ${OCR_CHECKSUM_PATTERN}"
changed=1
fi
if [ "$changed" -eq 1 ]; then
PACKAGE_BACKUP=$(mktemp)
cp "$pkg" "$PACKAGE_BACKUP"
mv "$tmp" "$pkg"
PACKAGE_PATCHED=1
success "package.json patched for publish"
else
rm -f "$tmp"
info "No overrides configured, publishing with default package.json"
fi
}
run_step "Patching package.json" "patch_package_json"
# ── Publish ───────────────────────────────────────────────────────────────────
do_publish() {
local registry_args=()
if [ -n "${OCR_PUBLISH_REGISTRY:-}" ]; then
registry_args=(--registry "$OCR_PUBLISH_REGISTRY")
fi
local pkg_name
pkg_name=$(jq -r '.name' "$PROJECT_ROOT/package.json")
local already
already=$(npm view "${pkg_name}@${NPM_VERSION}" version ${registry_args[@]+"${registry_args[@]}"} 2>/dev/null || true)
if [ "$already" = "$NPM_VERSION" ]; then
warn "Version ${NPM_VERSION} already published!"
if confirm "Skip publish and continue?"; then
return 0
else
die "Aborted."
fi
fi
info "Publishing ${pkg_name}@${NPM_VERSION} ..."
npm publish ${registry_args[@]+"${registry_args[@]}"}
success "Published ${pkg_name}@${NPM_VERSION}"
}
if [ "$SKIP_PUBLISH" = "1" ]; then
warn "Skipping publish (OCR_SKIP_PUBLISH=1)"
else
run_step "Publishing to registry" "do_publish"
fi
# ── Restore package.json ─────────────────────────────────────────────────────
if [ "$PACKAGE_PATCHED" -eq 1 ] && [ -n "$PACKAGE_BACKUP" ]; then
cp "$PACKAGE_BACKUP" "$PROJECT_ROOT/package.json"
rm -f "$PACKAGE_BACKUP"
success "package.json restored (version bump preserved)"
echo ""
fi
# ── Commit version bump (if version was synced) ──────────────────────────────
git add package.json 2>/dev/null || true
if ! git diff --cached --quiet 2>/dev/null; then
git commit -m "chore(release): bump version to ${VERSION_TAG}"
success "Committed version bump"
else
info "No version changes to commit."
fi
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}${BOLD}============================================${RESET}"
echo -e "${GREEN}${BOLD} PUBLISH COMPLETE ${RESET}"
echo -e "${GREEN}${BOLD}============================================${RESET}"
echo ""
echo -e " Version: ${BOLD}${VERSION_TAG}${RESET}"
[ -n "${OCR_PKG_NAME:-}" ] && echo -e " Package: ${OCR_PKG_NAME}" || echo -e " Package: $(jq -r '.name' "$PROJECT_ROOT/package.json")"
echo ""