chore(release): include cli install scripts

This commit is contained in:
Kayshen-X 2026-07-08 23:24:32 +08:00
parent a98458e146
commit d49087027e
4 changed files with 201 additions and 22 deletions

View file

@ -444,6 +444,7 @@ jobs:
echo "::error::NPM_TOKEN is required to publish web SDK packages"
exit 1
fi
echo "::notice::NPM_TOKEN must be scoped for @zseven-w packages and have bypass 2FA enabled"
printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN" > ~/.npmrc
npm whoami
- name: Build wasm-backed SDK packages
@ -529,6 +530,54 @@ jobs:
echo "::error::expected 3 SDK tarballs, found ${#sdk[@]}"
exit 1
fi
- name: Generate CLI install scripts
shell: bash
run: |
set -euo pipefail
tag="$GITHUB_REF_NAME"
version="${tag#v}"
sha_file() {
sha256sum "release-files/$1" | awk '{print $1}'
}
cli_mac_arm_sha="$(sha_file "op-cli-macos-aarch64.tar.gz")"
cli_mac_x64_sha="$(sha_file "op-cli-macos-x86_64.tar.gz")"
cli_linux_arm_sha="$(sha_file "op-cli-linux-aarch64.tar.gz")"
cli_linux_x64_sha="$(sha_file "op-cli-linux-x86_64.tar.gz")"
cli_win_arm_sha="$(sha_file "op-cli-windows-aarch64.zip")"
cli_win_x64_sha="$(sha_file "op-cli-windows-x86_64.zip")"
cp scripts/install-op.sh release-files/install-op.sh
cp scripts/install-op.ps1 release-files/install-op.ps1
OP_INSTALLER_VERSION="$version" \
OP_SHA_MACOS_AARCH64="$cli_mac_arm_sha" \
OP_SHA_MACOS_X86_64="$cli_mac_x64_sha" \
OP_SHA_LINUX_AARCH64="$cli_linux_arm_sha" \
OP_SHA_LINUX_X86_64="$cli_linux_x64_sha" \
perl -0pi -e '
s/^DEFAULT_OP_VERSION=.*/DEFAULT_OP_VERSION="$ENV{OP_INSTALLER_VERSION}"/m;
s/^DEFAULT_SHA_MACOS_AARCH64=.*/DEFAULT_SHA_MACOS_AARCH64="$ENV{OP_SHA_MACOS_AARCH64}"/m;
s/^DEFAULT_SHA_MACOS_X86_64=.*/DEFAULT_SHA_MACOS_X86_64="$ENV{OP_SHA_MACOS_X86_64}"/m;
s/^DEFAULT_SHA_LINUX_AARCH64=.*/DEFAULT_SHA_LINUX_AARCH64="$ENV{OP_SHA_LINUX_AARCH64}"/m;
s/^DEFAULT_SHA_LINUX_X86_64=.*/DEFAULT_SHA_LINUX_X86_64="$ENV{OP_SHA_LINUX_X86_64}"/m;
' release-files/install-op.sh
OP_INSTALLER_VERSION="$version" \
OP_SHA_WINDOWS_AARCH64="$cli_win_arm_sha" \
OP_SHA_WINDOWS_X86_64="$cli_win_x64_sha" \
perl -0pi -e '
s/^\$DefaultOpVersion = .*/\$DefaultOpVersion = "$ENV{OP_INSTALLER_VERSION}"/m;
s/^\$DefaultShaWindowsAarch64 = .*/\$DefaultShaWindowsAarch64 = "$ENV{OP_SHA_WINDOWS_AARCH64}"/m;
s/^\$DefaultShaWindowsX86_64 = .*/\$DefaultShaWindowsX86_64 = "$ENV{OP_SHA_WINDOWS_X86_64}"/m;
' release-files/install-op.ps1
chmod +x release-files/install-op.sh
if grep -E 'DEFAULT_OP_VERSION=""|DEFAULT_SHA_(MACOS|LINUX)_[A-Z0-9_]+=""|\$Default(OpVersion|ShaWindows[A-Za-z0-9_]+) = ""' \
release-files/install-op.sh release-files/install-op.ps1; then
echo "::error::CLI install scripts still contain unstamped release metadata"
exit 1
fi
grep -q "$version" release-files/install-op.sh
grep -q "$version" release-files/install-op.ps1
- name: Prepare release body
shell: bash
run: |

View file

@ -18,6 +18,21 @@
- Linux: `OpenPencil-0.8.0-<arch>-linux.AppImage`, `OpenPencil-0.8.0-<arch>-linux.deb`, plus raw desktop and `op` CLI archives.
- Web SDK: published npm packages and attached npm tarballs for the base SDK, React adapter, and Vue adapter.
- Package managers: Homebrew cask/formula and Scoop desktop/CLI manifests.
- CLI installers: `install-op.sh` and `install-op.ps1`.
## CLI Install
macOS / Linux:
```sh
curl -fsSL https://raw.githubusercontent.com/ZSeven-W/openpencil/main/scripts/install-op.sh | bash
```
Windows PowerShell:
```powershell
irm https://raw.githubusercontent.com/ZSeven-W/openpencil/main/scripts/install-op.ps1 | iex
```
## Docker

84
scripts/install-op.ps1 Normal file
View file

@ -0,0 +1,84 @@
param(
[string]$OpVersion = $env:OP_VERSION,
[string]$InstallDir = $(if ($env:INSTALL_DIR) { $env:INSTALL_DIR } else { Join-Path $env:USERPROFILE ".openpencil\bin" })
)
$ErrorActionPreference = "Stop"
$Owner = "ZSeven-W"
$Repo = "openpencil"
$DefaultOpVersion = ""
$DefaultShaWindowsAarch64 = ""
$DefaultShaWindowsX86_64 = ""
function Resolve-Version {
if (-not [string]::IsNullOrWhiteSpace($OpVersion)) {
return $OpVersion.TrimStart("v")
}
if (-not [string]::IsNullOrWhiteSpace($DefaultOpVersion)) {
return $DefaultOpVersion.TrimStart("v")
}
$Latest = Invoke-RestMethod -Uri "https://api.github.com/repos/$Owner/$Repo/releases/latest"
if (-not $Latest.tag_name) {
throw "install-op: could not resolve latest release tag; set OP_VERSION explicitly"
}
return $Latest.tag_name.TrimStart("v")
}
switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" {
$Label = "windows-x86_64"
$ExpectedSha = $DefaultShaWindowsX86_64
}
"ARM64" {
$Label = "windows-aarch64"
$ExpectedSha = $DefaultShaWindowsAarch64
}
default {
throw "install-op: unsupported Windows architecture $env:PROCESSOR_ARCHITECTURE"
}
}
$Version = Resolve-Version
$Asset = "op-cli-$Label.zip"
$Url = "https://github.com/$Owner/$Repo/releases/download/v$Version/$Asset"
Write-Host "==> Installing op $Version ($Label)"
Write-Host " from $Url"
$Temp = Join-Path ([System.IO.Path]::GetTempPath()) ("openpencil-op-install-" + [System.Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Path $Temp | Out-Null
try {
$Archive = Join-Path $Temp $Asset
Invoke-WebRequest -Uri $Url -OutFile $Archive -UseBasicParsing
if (-not [string]::IsNullOrWhiteSpace($ExpectedSha)) {
$ActualSha = (Get-FileHash -Algorithm SHA256 $Archive).Hash.ToLowerInvariant()
if ($ActualSha -ne $ExpectedSha) {
throw "install-op: checksum mismatch for $Asset. Expected $ExpectedSha, got $ActualSha"
}
}
Expand-Archive -Path $Archive -DestinationPath $Temp -Force
$Source = Get-ChildItem -Path $Temp -Filter "op.exe" -Recurse | Select-Object -First 1
if (-not $Source) {
throw "install-op: op.exe was not found in $Asset"
}
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
$Target = Join-Path $InstallDir "op.exe"
Copy-Item -Path $Source.FullName -Destination $Target -Force
$UserPath = [Environment]::GetEnvironmentVariable("Path", "User")
$PathEntries = @($UserPath -split ";" | Where-Object { $_ })
if ($PathEntries -notcontains $InstallDir) {
[Environment]::SetEnvironmentVariable("Path", (($PathEntries + $InstallDir) -join ";"), "User")
Write-Host "Added $InstallDir to the user PATH. Restart the shell to use op globally."
}
Write-Host "==> Done. Run 'op --version' to verify."
& $Target --version
} finally {
Remove-Item -Path $Temp -Recurse -Force -ErrorAction SilentlyContinue
}

View file

@ -6,13 +6,13 @@
# and installs the `op` binary into a bin directory on PATH.
#
# Usage:
# ./install-op.sh # install the latest release
# OP_VERSION=0.8.0 ./install-op.sh # pin a specific version
# INSTALL_DIR=$HOME/.local/bin ./install-op.sh # custom install dir
# ./install-op.sh # install the latest stable release
# OP_VERSION=0.8.0 ./install-op.sh # pin a specific version
# INSTALL_DIR=$HOME/.local/bin ./install-op.sh
#
# Environment overrides:
# OP_VERSION release version WITHOUT the leading "v" (default: latest)
# INSTALL_DIR install target directory (default: /usr/local/bin)
# The release workflow stamps DEFAULT_OP_VERSION and DEFAULT_SHA_* in the copy
# uploaded to GitHub Releases, so the release asset installs that exact tag and
# verifies the CLI archive checksum.
set -euo pipefail
@ -20,22 +20,25 @@ OWNER="ZSeven-W"
REPO="openpencil"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
# Resolve the asset "label" token rust-release.yml uses in
# op-cli-<label>.tar.gz: "<os>-<arch>" where os is macos/linux and arch is
# x86_64/aarch64 (the cargo target arch, NOT the cask's x64/arm64 token).
DEFAULT_OP_VERSION=""
DEFAULT_SHA_MACOS_AARCH64=""
DEFAULT_SHA_MACOS_X86_64=""
DEFAULT_SHA_LINUX_AARCH64=""
DEFAULT_SHA_LINUX_X86_64=""
detect_label() {
local os arch
case "$(uname -s)" in
Darwin) os="macos" ;;
Linux) os="linux" ;;
Linux) os="linux" ;;
*)
echo "error: unsupported OS '$(uname -s)' (only macOS and Linux are packaged)" >&2
exit 1
;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch="x86_64" ;;
arm64 | aarch64) arch="aarch64" ;;
x86_64 | amd64) arch="x86_64" ;;
arm64 | aarch64) arch="aarch64" ;;
*)
echo "error: unsupported architecture '$(uname -m)'" >&2
exit 1
@ -44,16 +47,18 @@ detect_label() {
printf '%s-%s' "$os" "$arch"
}
# Resolve the release version. Honors OP_VERSION; otherwise queries the
# GitHub API for the latest release tag and strips the leading "v".
resolve_version() {
if [ -n "${OP_VERSION:-}" ]; then
printf '%s' "$OP_VERSION"
return
fi
if [ -n "$DEFAULT_OP_VERSION" ]; then
printf '%s' "$DEFAULT_OP_VERSION"
return
fi
local api tag
api="https://api.github.com/repos/${OWNER}/${REPO}/releases/latest"
# Pull "tag_name": "vX.Y.Z" out of the JSON without a JSON parser.
tag="$(curl -fsSL "$api" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n1 | sed 's/.*"\(v\{0,1\}[^"]*\)"$/\1/')"
if [ -z "$tag" ]; then
echo "error: could not resolve the latest release tag from GitHub" >&2
@ -63,22 +68,52 @@ resolve_version() {
printf '%s' "${tag#v}"
}
expected_sha_for_label() {
case "$1" in
macos-aarch64) printf '%s' "$DEFAULT_SHA_MACOS_AARCH64" ;;
macos-x86_64) printf '%s' "$DEFAULT_SHA_MACOS_X86_64" ;;
linux-aarch64) printf '%s' "$DEFAULT_SHA_LINUX_AARCH64" ;;
linux-x86_64) printf '%s' "$DEFAULT_SHA_LINUX_X86_64" ;;
*) printf '' ;;
esac
}
sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | awk '{print $1}'
else
echo "error: sha256sum or shasum is required for checksum verification" >&2
exit 1
fi
}
main() {
local label version asset url tmp
local label version asset url tmp expected_sha actual_sha
label="$(detect_label)"
version="$(resolve_version)"
asset="op-cli-${label}.tar.gz"
url="https://github.com/${OWNER}/${REPO}/releases/download/v${version}/${asset}"
expected_sha="$(expected_sha_for_label "$label")"
echo "==> Installing op ${version} (${label})"
echo " from ${url}"
tmp="$(mktemp -d)"
# Always clean up the scratch dir, even on early exit.
trap 'rm -rf "$tmp"' EXIT
# Download and unpack. The tarball contains a single bare `op` binary.
curl -fsSL --retry 3 -o "$tmp/${asset}" "$url"
if [ -n "$expected_sha" ]; then
actual_sha="$(sha256_file "$tmp/${asset}")"
if [ "$actual_sha" != "$expected_sha" ]; then
echo "error: checksum mismatch for ${asset}" >&2
echo " expected: $expected_sha" >&2
echo " actual: $actual_sha" >&2
exit 1
fi
fi
tar -xzf "$tmp/${asset}" -C "$tmp"
if [ ! -f "$tmp/op" ]; then
echo "error: ${asset} did not contain an 'op' binary" >&2
@ -86,11 +121,7 @@ main() {
fi
chmod +x "$tmp/op"
# Install — use sudo automatically only when the target dir is not
# writable by the current user (e.g. the default /usr/local/bin).
echo "==> Installing to ${INSTALL_DIR}/op"
# Create the dir first (best effort) so the writability test below is
# meaningful for a not-yet-existing custom INSTALL_DIR.
mkdir -p "$INSTALL_DIR" 2>/dev/null || true
if [ -w "$INSTALL_DIR" ]; then
install -m 0755 "$tmp/op" "$INSTALL_DIR/op"