mirror of
https://github.com/langgenius/dify.git
synced 2026-05-26 03:47:42 +08:00
feat(api,web,cli): difyctl v1.0 — OAuth device flow, /openapi/v1 auth pipeline, CLI client
This commit is contained in:
149
cli/scripts/install-cli.ps1
Normal file
149
cli/scripts/install-cli.ps1
Normal file
@ -0,0 +1,149 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
One-line difyctl installer for Windows. Verifies sha256 before extract.
|
||||
.PARAMETER Version
|
||||
Dify release tag. Defaults to the latest release.
|
||||
.PARAMETER Prefix
|
||||
Install root. Defaults to $env:LOCALAPPDATA\difyctl.
|
||||
.PARAMETER Repo
|
||||
Release source repo. Defaults to langgenius/dify.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Version = $env:DIFYCTL_VERSION,
|
||||
[string]$Prefix = $env:DIFYCTL_PREFIX,
|
||||
[string]$Repo = $env:DIFYCTL_REPO
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ([string]::IsNullOrEmpty($Version)) { $Version = 'latest' }
|
||||
if ([string]::IsNullOrEmpty($Prefix)) { $Prefix = Join-Path $env:LOCALAPPDATA 'difyctl' }
|
||||
if ([string]::IsNullOrEmpty($Repo)) { $Repo = 'langgenius/dify' }
|
||||
|
||||
function Fail($msg) { Write-Error "install-cli: $msg"; exit 1 }
|
||||
function Need($cmd) {
|
||||
if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { Fail "$cmd is required" }
|
||||
}
|
||||
Need tar
|
||||
|
||||
switch ($env:PROCESSOR_ARCHITECTURE) {
|
||||
'AMD64' { $arch = 'x64' }
|
||||
'ARM64' { $arch = 'arm64' }
|
||||
default { Fail "unsupported arch: $env:PROCESSOR_ARCHITECTURE" }
|
||||
}
|
||||
$target = "win32-$arch"
|
||||
|
||||
if ($Version -eq 'latest') {
|
||||
$api = "https://api.github.com/repos/$Repo/releases/latest"
|
||||
} else {
|
||||
$api = "https://api.github.com/repos/$Repo/releases/tags/$Version"
|
||||
}
|
||||
|
||||
try {
|
||||
$release = Invoke-RestMethod -Uri $api -UseBasicParsing
|
||||
} catch {
|
||||
Fail "could not fetch release metadata from $api $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$tag = $release.tag_name
|
||||
if ([string]::IsNullOrEmpty($tag)) { Fail "release has no tag_name" }
|
||||
|
||||
$assetRegex = "^difyctl-v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?-$target\.tar\.xz$"
|
||||
$matches = @($release.assets | Where-Object { $_.name -match $assetRegex })
|
||||
|
||||
if ($matches.Count -eq 0) { Fail "no difyctl asset for $target on $tag" }
|
||||
if ($matches.Count -gt 1) {
|
||||
$names = ($matches | ForEach-Object { $_.name }) -join ', '
|
||||
Fail "expected exactly 1 difyctl asset for $target on $tag, found $($matches.Count): $names"
|
||||
}
|
||||
$asset = $matches[0].name
|
||||
|
||||
$suffix = "-$target.tar.xz"
|
||||
$cliV = $asset.Substring('difyctl-'.Length, $asset.Length - 'difyctl-'.Length - $suffix.Length)
|
||||
$checksums = "difyctl-$cliV-checksums.txt"
|
||||
|
||||
$checksumAsset = $release.assets | Where-Object { $_.name -eq $checksums } | Select-Object -First 1
|
||||
if ($null -eq $checksumAsset) {
|
||||
Fail "checksum file $checksums missing on $tag; refusing to install unverified binary"
|
||||
}
|
||||
|
||||
$url = "https://github.com/$Repo/releases/download/$tag/$asset"
|
||||
$sumsUrl = "https://github.com/$Repo/releases/download/$tag/$checksums"
|
||||
|
||||
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("difyctl-install-" + [guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
||||
$tarPath = Join-Path $tmp $asset
|
||||
$sumPath = Join-Path $tmp $checksums
|
||||
|
||||
try {
|
||||
Write-Host "downloading $asset"
|
||||
Invoke-WebRequest -Uri $url -OutFile $tarPath -UseBasicParsing
|
||||
Invoke-WebRequest -Uri $sumsUrl -OutFile $sumPath -UseBasicParsing
|
||||
|
||||
$expected = (Get-Content $sumPath | Where-Object { $_ -match " $([Regex]::Escape($asset))$" } | Select-Object -First 1)
|
||||
if ([string]::IsNullOrEmpty($expected)) { Fail "no checksum entry for $asset in $checksums" }
|
||||
$expectedHash = ($expected -split '\s+')[0].ToLower()
|
||||
$actualHash = (Get-FileHash -Algorithm SHA256 -Path $tarPath).Hash.ToLower()
|
||||
if ($expectedHash -ne $actualHash) {
|
||||
Fail "checksum mismatch for $asset (expected $expectedHash, got $actualHash)"
|
||||
}
|
||||
|
||||
if (Get-Command cosign -ErrorAction SilentlyContinue) {
|
||||
$sigUrl = "$url.sig"
|
||||
$pemUrl = "$url.pem"
|
||||
$sigPath = Join-Path $tmp "$asset.sig"
|
||||
$pemPath = Join-Path $tmp "$asset.pem"
|
||||
try {
|
||||
Invoke-WebRequest -Uri $sigUrl -OutFile $sigPath -UseBasicParsing
|
||||
Invoke-WebRequest -Uri $pemUrl -OutFile $pemPath -UseBasicParsing
|
||||
} catch {
|
||||
Fail "tarball signature/cert missing on $tag; refusing to install (cosign present): $($_.Exception.Message)"
|
||||
}
|
||||
$env:COSIGN_EXPERIMENTAL = '1'
|
||||
& cosign verify-blob `
|
||||
--certificate $pemPath `
|
||||
--signature $sigPath `
|
||||
--certificate-identity-regexp '^https://github.com/langgenius/dify/' `
|
||||
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' `
|
||||
$tarPath
|
||||
if ($LASTEXITCODE -ne 0) { Fail "cosign verification failed for $asset" }
|
||||
Write-Host "cosign: verified $asset"
|
||||
} else {
|
||||
Write-Host "note: cosign not installed; skipping signature verification (sha256 still enforced)"
|
||||
}
|
||||
|
||||
$shareDir = Join-Path $Prefix 'share'
|
||||
$binDir = Join-Path $Prefix 'bin'
|
||||
New-Item -ItemType Directory -Path $shareDir -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
|
||||
|
||||
Write-Host "extracting to $shareDir"
|
||||
& tar.exe -xJf $tarPath -C $shareDir --strip-components=1
|
||||
if ($LASTEXITCODE -ne 0) { Fail "tar.exe failed with exit $LASTEXITCODE" }
|
||||
|
||||
$sourceBin = Join-Path $shareDir 'bin\difyctl.cmd'
|
||||
if (-not (Test-Path $sourceBin)) { $sourceBin = Join-Path $shareDir 'bin\difyctl.exe' }
|
||||
if (-not (Test-Path $sourceBin)) { Fail "expected binary at bin\difyctl.{cmd,exe} after extract" }
|
||||
|
||||
$shimSrc = Get-Item $sourceBin
|
||||
Copy-Item -Path $sourceBin -Destination (Join-Path $binDir $shimSrc.Name) -Force
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp }
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "difyctl $cliV installed: $binDir"
|
||||
|
||||
$userPath = [System.Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if ($null -eq $userPath) { $userPath = '' }
|
||||
if (-not ($userPath -split ';' | Where-Object { $_ -ieq $binDir })) {
|
||||
$newPath = if ($userPath) { "$userPath;$binDir" } else { $binDir }
|
||||
[System.Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
||||
Write-Host "added $binDir to user PATH (open a new terminal to pick it up)"
|
||||
}
|
||||
else {
|
||||
Write-Host "verify: run 'difyctl version' in a new terminal"
|
||||
}
|
||||
132
cli/scripts/install-cli.sh
Executable file
132
cli/scripts/install-cli.sh
Executable file
@ -0,0 +1,132 @@
|
||||
#!/bin/sh
|
||||
# install-cli.sh — one-line difyctl installer for Linux and macOS.
|
||||
#
|
||||
# usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/langgenius/dify/main/cli/scripts/install-cli.sh | sh
|
||||
#
|
||||
# env: DIFYCTL_VERSION (default latest), DIFYCTL_PREFIX (default $HOME/.local),
|
||||
# DIFYCTL_REPO (default langgenius/dify).
|
||||
# requires: curl, tar (xz), uname, jq, sha256sum or shasum.
|
||||
|
||||
set -eu
|
||||
|
||||
REPO="${DIFYCTL_REPO:-langgenius/dify}"
|
||||
VERSION="${DIFYCTL_VERSION:-latest}"
|
||||
PREFIX="${DIFYCTL_PREFIX:-${HOME}/.local}"
|
||||
|
||||
err() { printf '%s\n' "install-cli: $*" >&2; }
|
||||
die() { err "$*"; exit 1; }
|
||||
need() { command -v "$1" >/dev/null 2>&1 || die "$1 is required"; }
|
||||
|
||||
need curl
|
||||
need tar
|
||||
need uname
|
||||
need jq
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
HASH="sha256sum"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
HASH="shasum -a 256"
|
||||
else
|
||||
die "need sha256sum or shasum"
|
||||
fi
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux*) os=linux ;;
|
||||
Darwin*) os=darwin ;;
|
||||
*) die "unsupported OS: $(uname -s)" ;;
|
||||
esac
|
||||
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch=x64 ;;
|
||||
arm64|aarch64) arch=arm64 ;;
|
||||
*) die "unsupported arch: $(uname -m)" ;;
|
||||
esac
|
||||
|
||||
target="${os}-${arch}"
|
||||
|
||||
if [ "$VERSION" = "latest" ]; then
|
||||
api="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
else
|
||||
api="https://api.github.com/repos/${REPO}/releases/tags/${VERSION}"
|
||||
fi
|
||||
|
||||
release=$(curl -fsSL "$api") || die "could not fetch release metadata from ${api}"
|
||||
tag=$(printf '%s' "$release" | jq -r '.tag_name')
|
||||
[ -n "$tag" ] && [ "$tag" != "null" ] || die "release has no tag_name"
|
||||
|
||||
matches=$(printf '%s' "$release" \
|
||||
| jq -r --arg t "$target" '.assets[].name | select(test("^difyctl-v[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?-\($t)\\.tar\\.xz$"))')
|
||||
count=$(printf '%s' "$matches" | grep -c . || true)
|
||||
case "$count" in
|
||||
0) die "no difyctl asset for ${target} on ${tag}" ;;
|
||||
1) asset="$matches" ;;
|
||||
*) die "expected exactly 1 difyctl asset for ${target} on ${tag}, found ${count}: ${matches}" ;;
|
||||
esac
|
||||
|
||||
no_target="${asset%-${target}.tar.xz}"
|
||||
cli_v="${no_target#difyctl-}"
|
||||
checksums="difyctl-${cli_v}-checksums.txt"
|
||||
|
||||
printf '%s' "$release" | jq -e --arg c "$checksums" '.assets[] | select(.name == $c)' >/dev/null \
|
||||
|| die "checksum file ${checksums} missing on ${tag}; refusing to install unverified binary"
|
||||
|
||||
url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
|
||||
sums_url="https://github.com/${REPO}/releases/download/${tag}/${checksums}"
|
||||
|
||||
tmp=$(mktemp -d 2>/dev/null || mktemp -d -t difyctl-install)
|
||||
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||
|
||||
printf 'downloading %s\n from %s\n' "$asset" "$url"
|
||||
curl -fsSL --retry 3 "$url" -o "${tmp}/${asset}"
|
||||
curl -fsSL --retry 3 "$sums_url" -o "${tmp}/${checksums}"
|
||||
|
||||
(
|
||||
cd "$tmp"
|
||||
grep " ${asset}\$" "$checksums" | $HASH -c -
|
||||
) || die "checksum mismatch for ${asset}"
|
||||
|
||||
if command -v cosign >/dev/null 2>&1; then
|
||||
sig_url="${url}.sig"
|
||||
pem_url="${url}.pem"
|
||||
curl -fsSL --retry 3 "$sig_url" -o "${tmp}/${asset}.sig" \
|
||||
|| die "tarball signature missing on ${tag}; refusing to install (cosign present)"
|
||||
curl -fsSL --retry 3 "$pem_url" -o "${tmp}/${asset}.pem" \
|
||||
|| die "tarball cert missing on ${tag}; refusing to install (cosign present)"
|
||||
COSIGN_EXPERIMENTAL=1 cosign verify-blob \
|
||||
--certificate "${tmp}/${asset}.pem" \
|
||||
--signature "${tmp}/${asset}.sig" \
|
||||
--certificate-identity-regexp '^https://github.com/langgenius/dify/' \
|
||||
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
|
||||
"${tmp}/${asset}" \
|
||||
|| die "cosign verification failed for ${asset}"
|
||||
printf 'cosign: verified %s\n' "$asset"
|
||||
else
|
||||
printf 'note: cosign not installed; skipping signature verification (sha256 still enforced)\n' >&2
|
||||
fi
|
||||
|
||||
share_dir="${PREFIX}/share/difyctl"
|
||||
bin_dir="${PREFIX}/bin"
|
||||
mkdir -p "$share_dir" "$bin_dir"
|
||||
|
||||
printf 'extracting to %s\n' "$share_dir"
|
||||
tar -xJf "${tmp}/${asset}" -C "$share_dir" --strip-components=1
|
||||
|
||||
target_bin="${share_dir}/bin/difyctl"
|
||||
[ -x "$target_bin" ] || die "expected binary at ${target_bin} after extract"
|
||||
|
||||
ln -sf "$target_bin" "${bin_dir}/difyctl"
|
||||
|
||||
printf '\ndifyctl %s installed: %s/difyctl\n' "$cli_v" "$bin_dir"
|
||||
|
||||
case ":${PATH}:" in
|
||||
*":${bin_dir}:"*)
|
||||
"${bin_dir}/difyctl" version >/dev/null 2>&1 \
|
||||
&& printf 'verify: run "difyctl version"\n' \
|
||||
|| err "binary present but failed to execute; check ${bin_dir}/difyctl"
|
||||
;;
|
||||
*)
|
||||
printf '\n%s is not on your PATH. Add this to your shell profile:\n' "$bin_dir"
|
||||
printf ' export PATH="%s:$PATH"\n' "$bin_dir"
|
||||
;;
|
||||
esac
|
||||
22
cli/scripts/lib/common.sh
Executable file
22
cli/scripts/lib/common.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/lib/common.sh — shared shell helpers for cli/ scripts.
|
||||
|
||||
[[ -n "${DIFYCTL_LIB_COMMON_SH:-}" ]] && return 0
|
||||
readonly DIFYCTL_LIB_COMMON_SH=1
|
||||
|
||||
log::info() { printf '\033[36m[info]\033[0m %s\n' "$*" >&2; }
|
||||
log::warn() { printf '\033[33m[warn]\033[0m %s\n' "$*" >&2; }
|
||||
log::err() { printf '\033[31m[err ]\033[0m %s\n' "$*" >&2; }
|
||||
|
||||
die() { log::err "$*"; exit 1; }
|
||||
|
||||
# Resolve the cli/ directory (parent of scripts/).
|
||||
cli::root() {
|
||||
local dir
|
||||
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
printf '%s' "$dir"
|
||||
}
|
||||
|
||||
require() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1${2:+ — $2}"
|
||||
}
|
||||
66
cli/scripts/lib/resolve-buildinfo.ts
Normal file
66
cli/scripts/lib/resolve-buildinfo.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import type { ExecSyncOptions } from 'node:child_process'
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
export const BUILD_CHANNELS = ['dev', 'rc', 'stable'] as const
|
||||
export type BuildChannel = (typeof BUILD_CHANNELS)[number]
|
||||
|
||||
export type BuildInfo = {
|
||||
version: string
|
||||
commit: string
|
||||
buildDate: string
|
||||
channel: BuildChannel
|
||||
minDify: string
|
||||
maxDify: string
|
||||
}
|
||||
|
||||
export type Env = Record<string, string | undefined>
|
||||
|
||||
export type GitProbe = (cmd: string) => string | null
|
||||
|
||||
const GIT_PROBE_OPTS: ExecSyncOptions = {
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}
|
||||
|
||||
export const defaultGitProbe: GitProbe = (cmd) => {
|
||||
try {
|
||||
return execSync(cmd, GIT_PROBE_OPTS).toString().trim() || null
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export type ResolveOptions = {
|
||||
env?: Env
|
||||
git?: GitProbe
|
||||
now?: () => Date
|
||||
}
|
||||
|
||||
export function resolveBuildInfo(opts: ResolveOptions = {}): BuildInfo {
|
||||
const env = opts.env ?? process.env
|
||||
const git = opts.git ?? defaultGitProbe
|
||||
const now = opts.now ?? (() => new Date())
|
||||
|
||||
const channel = env.DIFYCTL_CHANNEL ?? 'dev'
|
||||
if (!(BUILD_CHANNELS as readonly string[]).includes(channel)) {
|
||||
throw new Error(
|
||||
`invalid DIFYCTL_CHANNEL: ${channel} (expected ${BUILD_CHANNELS.join(' | ')})`,
|
||||
)
|
||||
}
|
||||
|
||||
const version
|
||||
= env.DIFYCTL_VERSION
|
||||
?? git('git describe --tags --dirty --always')
|
||||
?? '0.0.0-dev'
|
||||
|
||||
const commit
|
||||
= env.DIFYCTL_COMMIT
|
||||
?? git('git rev-parse HEAD')
|
||||
?? 'none'
|
||||
|
||||
const buildDate = env.DIFYCTL_BUILD_DATE ?? now().toISOString()
|
||||
const minDify = env.DIFYCTL_MIN_DIFY ?? '0.0.0'
|
||||
const maxDify = env.DIFYCTL_MAX_DIFY ?? '0.0.0'
|
||||
|
||||
return { version, commit, buildDate, channel: channel as BuildChannel, minDify, maxDify }
|
||||
}
|
||||
9
cli/scripts/print-buildinfo.ts
Normal file
9
cli/scripts/print-buildinfo.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { resolveBuildInfo } from './lib/resolve-buildinfo.js'
|
||||
|
||||
const info = resolveBuildInfo()
|
||||
process.stdout.write(
|
||||
`version: ${info.version}\n`
|
||||
+ `commit: ${info.commit}\n`
|
||||
+ `built: ${info.buildDate}\n`
|
||||
+ `channel: ${info.channel}\n`,
|
||||
)
|
||||
54
cli/scripts/release-bump-guard.sh
Executable file
54
cli/scripts/release-bump-guard.sh
Executable file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release-bump-guard.sh — auto-path only. Refuse if version+compat
|
||||
# both unchanged vs. the channel-matching npm dist-tag.
|
||||
#
|
||||
# Required env: NEW_VERSION, NEW_MIN_DIFY, NEW_MAX_DIFY.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
require node
|
||||
require jq
|
||||
require npm
|
||||
|
||||
cd "$(cli::root)"
|
||||
|
||||
: "${NEW_VERSION:?NEW_VERSION is required}"
|
||||
: "${NEW_MIN_DIFY:?NEW_MIN_DIFY is required}"
|
||||
: "${NEW_MAX_DIFY:?NEW_MAX_DIFY is required}"
|
||||
|
||||
channel=$(node -p "require('./package.json').difyctl.channel")
|
||||
case "$channel" in
|
||||
stable) dist_tag=latest ;;
|
||||
rc) dist_tag=next ;;
|
||||
*) die "unsupported channel for publish: ${channel}" ;;
|
||||
esac
|
||||
|
||||
dist_tags_json=$(npm view @langgenius/difyctl dist-tags --json 2>/dev/null || echo '{}')
|
||||
prev_version=$(echo "$dist_tags_json" | jq -r --arg t "$dist_tag" '.[$t] // ""')
|
||||
|
||||
if [[ -z "$prev_version" ]]; then
|
||||
log::info "no prior release on dist-tag '${dist_tag}'; skipping bump guard"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$prev_version" == "$NEW_VERSION" ]]; then
|
||||
echo "::warning title=cli version not bumped::package.json version ${NEW_VERSION} is already published on dist-tag '${dist_tag}'. If this is a deliberate re-run (dify release re-cut, retry after failure), ignore. If you intended to ship new cli bytes, bump cli/package.json#version and re-run."
|
||||
log::info "same version as npm dist-tag '${dist_tag}' (${prev_version}); skipping bump guard"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
prev_meta=$(npm view "@langgenius/difyctl@${prev_version}" --json)
|
||||
prev_min=$(echo "$prev_meta" | jq -r '.difyctl.compat.minDify')
|
||||
prev_max=$(echo "$prev_meta" | jq -r '.difyctl.compat.maxDify')
|
||||
|
||||
[[ "$NEW_VERSION" != "$prev_version" ]] \
|
||||
|| die "version unchanged from npm dist-tag '${dist_tag}' (${prev_version}); bump cli/package.json"
|
||||
|
||||
[[ "$NEW_MIN_DIFY" != "$prev_min" || "$NEW_MAX_DIFY" != "$prev_max" ]] \
|
||||
|| die "compat unchanged from npm @${prev_version} on dist-tag '${dist_tag}' (${prev_min}..${prev_max}); bump in cli/package.json"
|
||||
|
||||
log::info "bump guard passed: ${prev_version} → ${NEW_VERSION}, compat ${prev_min}..${prev_max} → ${NEW_MIN_DIFY}..${NEW_MAX_DIFY}"
|
||||
33
cli/scripts/release-cosign-sign.sh
Executable file
33
cli/scripts/release-cosign-sign.sh
Executable file
@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release-cosign-sign.sh — keyless cosign sign of tarballs + checksum
|
||||
# manifest using GitHub Actions OIDC (Sigstore Fulcio cert + Rekor log entry).
|
||||
#
|
||||
# Required env: CLI_VERSION. Workflow must export id-token: write and set
|
||||
# COSIGN_EXPERIMENTAL=1 (cli-release.yml does both).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
require cosign
|
||||
|
||||
: "${CLI_VERSION:?CLI_VERSION is required}"
|
||||
|
||||
cd "$(cli::root)/dist"
|
||||
|
||||
shopt -s nullglob
|
||||
targets=(difyctl-v"${CLI_VERSION}"-*.tar.xz "difyctl-v${CLI_VERSION}-checksums.txt")
|
||||
shopt -u nullglob
|
||||
|
||||
[[ ${#targets[@]} -gt 0 ]] || die "no files to sign in dist/ for CLI_VERSION=${CLI_VERSION}"
|
||||
|
||||
for f in "${targets[@]}"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
cosign sign-blob --yes \
|
||||
--output-signature "${f}.sig" \
|
||||
--output-certificate "${f}.pem" \
|
||||
"$f"
|
||||
log::info "signed ${f} → ${f}.sig + ${f}.pem"
|
||||
done
|
||||
48
cli/scripts/release-npm-publish.sh
Executable file
48
cli/scripts/release-npm-publish.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release-npm-publish.sh — channel-aware npm publish with
|
||||
# EPUBLISHCONFLICT no-op trap.
|
||||
#
|
||||
# Required env: CHANNEL (stable | rc), NEW_VERSION.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
require npm
|
||||
require node
|
||||
|
||||
cd "$(cli::root)"
|
||||
|
||||
: "${CHANNEL:?CHANNEL is required}"
|
||||
: "${NEW_VERSION:?NEW_VERSION is required}"
|
||||
|
||||
case "$CHANNEL" in
|
||||
stable) dist_tag=latest ;;
|
||||
rc) dist_tag=next ;;
|
||||
*) die "unsupported channel for publish: ${CHANNEL}" ;;
|
||||
esac
|
||||
|
||||
pkg_version=$(node -p "require('./package.json').version")
|
||||
[[ "$pkg_version" == "$NEW_VERSION" ]] \
|
||||
|| die "package.json version (${pkg_version}) != NEW_VERSION (${NEW_VERSION})"
|
||||
|
||||
set +e
|
||||
output=$(npm publish --access public --provenance --tag "$dist_tag" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
|
||||
if [[ $status -eq 0 ]]; then
|
||||
log::info "PUBLISHED @langgenius/difyctl@${NEW_VERSION} --tag ${dist_tag}"
|
||||
printf '%s\n' "$output"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if printf '%s' "$output" | grep -qE 'EPUBLISHCONFLICT|cannot publish over the previously published versions'; then
|
||||
log::warn "NO-OP: @langgenius/difyctl@${NEW_VERSION} already on registry (idempotent re-run)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$output" >&2
|
||||
die "npm publish failed (exit ${status}); see output above"
|
||||
99
cli/scripts/release-upload-tarballs.sh
Executable file
99
cli/scripts/release-upload-tarballs.sh
Executable file
@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release-upload-tarballs.sh — idempotent gh release upload of
|
||||
# tarballs + checksum file (sha256-strict; skip on match, fail on mismatch)
|
||||
# and cosign .sig/.pem signatures (overwrite-allowed; bytes vary per run).
|
||||
#
|
||||
# Required env: DIFY_TAG, CLI_VERSION, GH_TOKEN.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
require gh
|
||||
require jq
|
||||
|
||||
: "${DIFY_TAG:?DIFY_TAG is required}"
|
||||
: "${CLI_VERSION:?CLI_VERSION is required}"
|
||||
|
||||
cd "$(cli::root)"
|
||||
|
||||
REPO_FLAG=(--repo langgenius/dify)
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
hash_cmd() { sha256sum "$1" | awk '{print $1}'; }
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
hash_cmd() { shasum -a 256 "$1" | awk '{print $1}'; }
|
||||
else
|
||||
die "no sha256 hasher found (need sha256sum or shasum)"
|
||||
fi
|
||||
|
||||
remote_json=$(gh release view "$DIFY_TAG" "${REPO_FLAG[@]}" --json assets -q '.assets')
|
||||
|
||||
upload_one() {
|
||||
local file="$1"
|
||||
local mode="${2:-strict}" # strict | clobber
|
||||
local name
|
||||
name=$(basename "$file")
|
||||
local local_sha
|
||||
local_sha=$(hash_cmd "$file")
|
||||
local remote_entry
|
||||
remote_entry=$(printf '%s' "$remote_json" | jq -c --arg n "$name" '.[] | select(.name == $n)')
|
||||
|
||||
if [[ -z "$remote_entry" ]]; then
|
||||
log::info "uploading ${name}"
|
||||
gh release upload "$DIFY_TAG" "$file" "${REPO_FLAG[@]}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "clobber" ]]; then
|
||||
log::info "overwriting ${name} (clobber mode — cosign sig/cert)"
|
||||
gh release upload "$DIFY_TAG" "$file" "${REPO_FLAG[@]}" --clobber
|
||||
return
|
||||
fi
|
||||
|
||||
local remote_digest remote_sha=""
|
||||
remote_digest=$(printf '%s' "$remote_entry" | jq -r '.digest // ""')
|
||||
if [[ "$remote_digest" == sha256:* ]]; then
|
||||
remote_sha="${remote_digest#sha256:}"
|
||||
else
|
||||
local tmp download_url
|
||||
tmp=$(mktemp)
|
||||
download_url=$(printf '%s' "$remote_entry" | jq -r '.url')
|
||||
gh api -H 'Accept: application/octet-stream' "$download_url" > "$tmp"
|
||||
remote_sha=$(hash_cmd "$tmp")
|
||||
rm -f "$tmp"
|
||||
fi
|
||||
|
||||
if [[ "$local_sha" == "$remote_sha" ]]; then
|
||||
log::info "skip ${name} (sha256 matches)"
|
||||
return
|
||||
fi
|
||||
|
||||
die "asset ${name} already on ${DIFY_TAG} with different sha256 (local=${local_sha}, remote=${remote_sha}); refusing to overwrite"
|
||||
}
|
||||
|
||||
shopt -s nullglob
|
||||
tars=(dist/difyctl-v"${CLI_VERSION}"-*.tar.xz)
|
||||
checksum_file="dist/difyctl-v${CLI_VERSION}-checksums.txt"
|
||||
sigs=(dist/difyctl-v"${CLI_VERSION}"-*.sig dist/difyctl-v"${CLI_VERSION}"-checksums.txt.sig)
|
||||
pems=(dist/difyctl-v"${CLI_VERSION}"-*.pem dist/difyctl-v"${CLI_VERSION}"-checksums.txt.pem)
|
||||
shopt -u nullglob
|
||||
|
||||
[[ ${#tars[@]} -gt 0 ]] || die "no tarballs in dist/ matching difyctl-v${CLI_VERSION}-*.tar.xz"
|
||||
[[ -f "$checksum_file" ]] || die "checksum file missing: ${checksum_file}"
|
||||
|
||||
for f in "${tars[@]}" "$checksum_file"; do
|
||||
upload_one "$f" strict
|
||||
done
|
||||
|
||||
# Cosign signatures + certs are keyless and re-generated per run with fresh
|
||||
# timestamps; their bytes change each run but verify the same blob. Allow
|
||||
# overwrite via --clobber so re-runs converge cleanly.
|
||||
for f in "${sigs[@]}" "${pems[@]}"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
upload_one "$f" clobber
|
||||
done
|
||||
|
||||
log::info "uploaded ${#tars[@]} tarballs + checksums.txt + signatures to dify release ${DIFY_TAG}"
|
||||
43
cli/scripts/release-validate-manifest.sh
Executable file
43
cli/scripts/release-validate-manifest.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release-validate-manifest.sh — validate cli/package.json release fields.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
cd "$(cli::root)"
|
||||
|
||||
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
|
||||
|
||||
version=$(node -p "require('./package.json').version")
|
||||
channel=$(node -p "require('./package.json').difyctl.channel")
|
||||
min_dify=$(node -p "require('./package.json').difyctl.compat.minDify")
|
||||
max_dify=$(node -p "require('./package.json').difyctl.compat.maxDify")
|
||||
|
||||
[[ "$version" =~ $SEMVER_RE ]] || die "invalid version: ${version}"
|
||||
|
||||
case "$channel" in
|
||||
rc|stable) ;;
|
||||
*) die "invalid difyctl.channel: ${channel} (expected rc | stable)" ;;
|
||||
esac
|
||||
|
||||
[[ "$min_dify" =~ $SEMVER_RE ]] || die "invalid difyctl.compat.minDify: ${min_dify}"
|
||||
[[ "$max_dify" =~ $SEMVER_RE ]] || die "invalid difyctl.compat.maxDify: ${max_dify}"
|
||||
|
||||
case "$min_dify" in *[xX*]*) die "wildcards not allowed in minDify: ${min_dify}" ;; esac
|
||||
case "$max_dify" in *[xX*]*) die "wildcards not allowed in maxDify: ${max_dify}" ;; esac
|
||||
|
||||
cmp=$(node -e "
|
||||
const a = process.argv[1].split('-')[0].split('.').map(Number)
|
||||
const b = process.argv[2].split('-')[0].split('.').map(Number)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a[i] !== b[i]) { console.log(a[i] < b[i] ? -1 : 1); process.exit(0) }
|
||||
}
|
||||
console.log(0)
|
||||
" "$min_dify" "$max_dify")
|
||||
|
||||
[[ "$cmp" -le 0 ]] || die "minDify (${min_dify}) > maxDify (${max_dify})"
|
||||
|
||||
log::info "manifest valid: version=${version} channel=${channel} compat=${min_dify}..${max_dify}"
|
||||
37
cli/scripts/release-write-checksums.sh
Executable file
37
cli/scripts/release-write-checksums.sh
Executable file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release-write-checksums.sh — write sha256 manifest for tarballs.
|
||||
#
|
||||
# Required env: CLI_VERSION (e.g. 0.1.0-rc.1). Output:
|
||||
# cli/dist/difyctl-v<CLI_VERSION>-checksums.txt
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
: "${CLI_VERSION:?CLI_VERSION is required}"
|
||||
|
||||
cd "$(cli::root)/dist"
|
||||
|
||||
manifest="difyctl-v${CLI_VERSION}-checksums.txt"
|
||||
> "$manifest"
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
hash_cmd="sha256sum"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
hash_cmd="shasum -a 256"
|
||||
else
|
||||
die "no sha256 hasher found (need sha256sum or shasum)"
|
||||
fi
|
||||
|
||||
found=0
|
||||
for tar in difyctl-v"${CLI_VERSION}"-*.tar.xz; do
|
||||
[[ -f "$tar" ]] || continue
|
||||
$hash_cmd "$tar" >> "$manifest"
|
||||
found=$((found + 1))
|
||||
done
|
||||
|
||||
[[ "$found" -gt 0 ]] || die "no tarballs matching difyctl-v${CLI_VERSION}-*.tar.xz in dist/"
|
||||
|
||||
log::info "wrote ${manifest} (${found} entries)"
|
||||
38
cli/scripts/release.sh
Executable file
38
cli/scripts/release.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/release.sh — local-developer release build.
|
||||
#
|
||||
# Reads cli/package.json, validates, exports DIFYCTL_* env, runs pnpm build +
|
||||
# oclif pack tarballs. cli-release.yml does NOT call this; the workflow inlines
|
||||
# the same env contract.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "${_dir}/lib/common.sh"
|
||||
|
||||
require pnpm 'install with `corepack enable && corepack prepare pnpm@latest --activate`'
|
||||
require node
|
||||
|
||||
cd "$(cli::root)"
|
||||
|
||||
scripts/release-validate-manifest.sh
|
||||
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
CHANNEL=$(node -p "require('./package.json').difyctl.channel")
|
||||
MIN_DIFY=$(node -p "require('./package.json').difyctl.compat.minDify")
|
||||
MAX_DIFY=$(node -p "require('./package.json').difyctl.compat.maxDify")
|
||||
|
||||
export DIFYCTL_VERSION="${PKG_VERSION}"
|
||||
export DIFYCTL_CHANNEL="${CHANNEL}"
|
||||
export DIFYCTL_MIN_DIFY="${MIN_DIFY}"
|
||||
export DIFYCTL_MAX_DIFY="${MAX_DIFY}"
|
||||
export DIFYCTL_COMMIT="$(git rev-parse HEAD)"
|
||||
export DIFYCTL_BUILD_DATE="$(git log -1 --format=%cI HEAD)"
|
||||
|
||||
log::info "release ${DIFYCTL_VERSION} (channel=${DIFYCTL_CHANNEL}, compat=${MIN_DIFY}..${MAX_DIFY})"
|
||||
pnpm build
|
||||
pnpm pack:tarballs
|
||||
|
||||
log::info "artifacts in dist/"
|
||||
ls -lh dist/ 2>/dev/null | tail -n +2 >&2 || true
|
||||
44
cli/scripts/run-smoke.ts
Normal file
44
cli/scripts/run-smoke.ts
Normal file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env -S node --import tsx
|
||||
import { execSync } from 'node:child_process'
|
||||
|
||||
type Check = { name: string, run: () => void }
|
||||
|
||||
const baseUrlIdx = process.argv.indexOf('--base-url')
|
||||
const baseUrl = baseUrlIdx > -1 ? process.argv[baseUrlIdx + 1] : 'http://localhost:5001'
|
||||
if (!baseUrl) {
|
||||
console.error('usage: run-smoke.ts --base-url <url>')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const env = { ...process.env, DIFY_BASE_URL: baseUrl }
|
||||
|
||||
function cli(args: string): string {
|
||||
return execSync(`pnpm exec tsx bin/dev.js ${args}`, { env, encoding: 'utf8' })
|
||||
}
|
||||
|
||||
const checks: Check[] = [
|
||||
{ name: 'config show', run: () => { cli('config show') } },
|
||||
{ name: 'get workspace', run: () => {
|
||||
if (!cli('get workspace').includes('id'))
|
||||
throw new Error('no workspace listed')
|
||||
} },
|
||||
{ name: 'get apps', run: () => { cli('get apps') } },
|
||||
{ name: 'difyctl version prints compat', run: () => {
|
||||
if (!cli('version').includes('compat:'))
|
||||
throw new Error('no compat line')
|
||||
} },
|
||||
]
|
||||
|
||||
let failed = 0
|
||||
for (const c of checks) {
|
||||
try {
|
||||
c.run()
|
||||
console.log(`[x] ${c.name}`)
|
||||
}
|
||||
catch (err) {
|
||||
failed++
|
||||
console.log(`[ ] ${c.name} — ${(err as Error).message}`)
|
||||
}
|
||||
}
|
||||
console.log(`\n${checks.length - failed}/${checks.length} checks passed`)
|
||||
process.exit(failed > 0 ? 1 : 0)
|
||||
Reference in New Issue
Block a user