From ce9511eed6a48528ddcac4be1b335ebe517abf83 Mon Sep 17 00:00:00 2001 From: kite Date: Thu, 21 May 2026 17:33:46 +0800 Subject: [PATCH] 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. --- .gitignore | 3 +- .npmignore | 1 + scripts/publish/.env.example | 20 +++ scripts/publish/_common.sh | 64 ++++++++ scripts/publish/publish.sh | 297 +++++++++++++++++++++++++++++++++++ 5 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 scripts/publish/.env.example create mode 100755 scripts/publish/_common.sh create mode 100755 scripts/publish/publish.sh diff --git a/.gitignore b/.gitignore index eca34e0..91ce8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.npmignore b/.npmignore index ee25055..3c69822 100644 --- a/.npmignore +++ b/.npmignore @@ -6,6 +6,7 @@ Makefile go.mod go.sum scripts/hooks/ +scripts/publish/ .claude/ .git/ .idea/ diff --git a/scripts/publish/.env.example b/scripts/publish/.env.example new file mode 100644 index 0000000..b360b65 --- /dev/null +++ b/scripts/publish/.env.example @@ -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= diff --git a/scripts/publish/_common.sh b/scripts/publish/_common.sh new file mode 100755 index 0000000..64b369e --- /dev/null +++ b/scripts/publish/_common.sh @@ -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 "" +} diff --git a/scripts/publish/publish.sh b/scripts/publish/publish.sh new file mode 100755 index 0000000..6badd87 --- /dev/null +++ b/scripts/publish/publish.sh @@ -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 ""