chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
// Backfill area/platform/severity labels on existing open issues via the DeepSeek
|
||||
// API — the one-off companion to .github/workflows/issue-auto-label.yml (which
|
||||
// only fires on new issues). Keep the label sets and prompt in sync with that
|
||||
// workflow. GitHub access uses the local `gh` CLI (must be authenticated).
|
||||
//
|
||||
// Usage:
|
||||
// DEEPSEEK_API_KEY=... node scripts/backfill-issue-labels.mjs [options]
|
||||
// Options:
|
||||
// --dry-run print what would change, apply nothing
|
||||
// --only-unlabeled skip issues that already have an area label
|
||||
// --limit N process at most N issues (default 200)
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const onlyUnlabeled = args.includes('--only-unlabeled');
|
||||
const li = args.indexOf('--limit');
|
||||
const limit = li >= 0 ? parseInt(args[li + 1], 10) : 200;
|
||||
|
||||
const KEY = process.env.DEEPSEEK_API_KEY;
|
||||
if (!KEY) {
|
||||
console.error('DEEPSEEK_API_KEY is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const AREA = ['agent', 'mcp', 'config', 'updater', 'provider', 'desktop', 'tui', 'skills', 'rendering'];
|
||||
const PLATFORM = ['windows', 'macos', 'linux'];
|
||||
const SEVERITY = ['crash', 'data-loss', 'security'];
|
||||
const ALLOWED = new Set([...AREA, ...PLATFORM, ...SEVERITY]);
|
||||
|
||||
const SYSTEM = [
|
||||
'You categorize GitHub issues for Reasonix, a Go-based AI coding agent with a Wails desktop app and a terminal UI.',
|
||||
'Pick labels ONLY from these fixed sets. Never invent labels.',
|
||||
'area (0-2, the affected subsystem):',
|
||||
' agent: core agent loop / tool-calling / reasoning',
|
||||
' mcp: MCP servers and plugins',
|
||||
' config: configuration, setup wizard, .toml/.env',
|
||||
' updater: auto-update, installer, release packaging',
|
||||
' provider: model providers, model selection/switching',
|
||||
' desktop: Wails desktop GUI',
|
||||
' tui: terminal UI / CLI',
|
||||
' skills: skills system',
|
||||
' rendering: terminal rendering / flicker / repaint',
|
||||
'platform (only if clearly specific to one OS): windows, macos, linux',
|
||||
'severity (only if clearly applicable):',
|
||||
' crash: app crashes, hangs, or freezes',
|
||||
' data-loss: loss of sessions, config, or history',
|
||||
' security: credential/secret exposure or a security flaw',
|
||||
'Be conservative: omit a label when unsure. The issue may be in Chinese.',
|
||||
'Reply with JSON only: {"area":[],"platform":[],"severity":[]}',
|
||||
].join('\n');
|
||||
|
||||
function gh(args) {
|
||||
return execFileSync('gh', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
async function classify(title, body) {
|
||||
const res = await fetch('https://api.deepseek.com/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
temperature: 0,
|
||||
response_format: { type: 'json_object' },
|
||||
messages: [
|
||||
{ role: 'system', content: SYSTEM },
|
||||
{ role: 'user', content: `Title: ${title}\n\nBody:\n${body}` },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`DeepSeek API ${res.status}: ${await res.text()}`);
|
||||
const data = await res.json();
|
||||
const parsed = JSON.parse(data.choices[0].message.content);
|
||||
return [...(parsed.area || []), ...(parsed.platform || []), ...(parsed.severity || [])].filter((l) => ALLOWED.has(l));
|
||||
}
|
||||
|
||||
const issues = JSON.parse(
|
||||
gh(['issue', 'list', '--state', 'open', '--limit', String(limit), '--json', 'number,title,body,labels']),
|
||||
);
|
||||
console.log(`${issues.length} open issues; dryRun=${dryRun} onlyUnlabeled=${onlyUnlabeled}`);
|
||||
|
||||
let changed = 0;
|
||||
for (const it of issues) {
|
||||
const existing = it.labels.map((l) => l.name);
|
||||
if (onlyUnlabeled && existing.some((l) => AREA.includes(l))) {
|
||||
continue;
|
||||
}
|
||||
let labels;
|
||||
try {
|
||||
labels = await classify(it.title, (it.body || '').slice(0, 4000));
|
||||
} catch (e) {
|
||||
console.warn(`#${it.number}: classify failed: ${e.message}`);
|
||||
continue;
|
||||
}
|
||||
if (!labels.some((l) => AREA.includes(l))) labels.push('needs-triage');
|
||||
const toAdd = labels.filter((l) => !existing.includes(l));
|
||||
if (!toAdd.length) {
|
||||
console.log(`#${it.number}: nothing new`);
|
||||
continue;
|
||||
}
|
||||
if (dryRun) {
|
||||
console.log(`#${it.number}: would add ${toAdd.join(', ')} — ${it.title.slice(0, 50)}`);
|
||||
} else {
|
||||
gh(['issue', 'edit', String(it.number), ...toAdd.flatMap((l) => ['--add-label', l])]);
|
||||
console.log(`#${it.number}: +${toAdd.join(', ')}`);
|
||||
}
|
||||
changed++;
|
||||
}
|
||||
console.log(`Done. ${changed} issue(s) ${dryRun ? 'would be' : ''} updated.`);
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
tmp=$(mktemp)
|
||||
trap 'rm -f "$tmp"' EXIT
|
||||
|
||||
set +e
|
||||
REASONIX_RELEASE_CACHE_GUARD=1 go test ./internal/agent -run '^TestReleaseCacheHitGuard$' -v -count=1 2>&1 | tee "$tmp"
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
{
|
||||
echo "### Cache guard"
|
||||
echo
|
||||
echo '```'
|
||||
grep 'CACHE_GUARD_RESULT:' "$tmp" || true
|
||||
grep 'CACHE_GUARD_WARNING:' "$tmp" || true
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
while IFS= read -r line; do
|
||||
msg=${line#*CACHE_GUARD_WARNING: }
|
||||
echo "::warning title=Reasonix cache guard::$msg"
|
||||
done < <(grep 'CACHE_GUARD_WARNING:' "$tmp" || true)
|
||||
Executable
+188
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/check-cache-impact.sh [changed-file ...]
|
||||
|
||||
Checks that PRs touching cache-sensitive prompt or tool surfaces include an
|
||||
explicit cache-impact note and guard-test note in the pull request body.
|
||||
|
||||
Inputs:
|
||||
CACHE_IMPACT_PR_BODY or PR_BODY Pull request body text.
|
||||
CACHE_IMPACT_PR_BODY_FILE File containing the pull request body.
|
||||
CACHE_IMPACT_CHANGED_FILES Newline-separated changed files.
|
||||
CACHE_IMPACT_CHANGED_FILES_FILE File containing newline-separated changed files.
|
||||
CACHE_IMPACT_BASE_SHA / BASE_SHA Diff base when files are not supplied.
|
||||
CACHE_IMPACT_HEAD_SHA / HEAD_SHA Diff head when files are not supplied.
|
||||
USAGE
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
body="${CACHE_IMPACT_PR_BODY:-${PR_BODY:-}}"
|
||||
if [[ -n "${CACHE_IMPACT_PR_BODY_FILE:-}" ]]; then
|
||||
body="$(cat "$CACHE_IMPACT_PR_BODY_FILE")"
|
||||
fi
|
||||
|
||||
changed_input=""
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
changed_input="$(printf '%s\n' "$@")"
|
||||
elif [[ -n "${CACHE_IMPACT_CHANGED_FILES_FILE:-}" ]]; then
|
||||
changed_input="$(cat "$CACHE_IMPACT_CHANGED_FILES_FILE")"
|
||||
elif [[ -n "${CACHE_IMPACT_CHANGED_FILES:-}" ]]; then
|
||||
changed_input="$CACHE_IMPACT_CHANGED_FILES"
|
||||
else
|
||||
base="${CACHE_IMPACT_BASE_SHA:-${BASE_SHA:-}}"
|
||||
head="${CACHE_IMPACT_HEAD_SHA:-${HEAD_SHA:-HEAD}}"
|
||||
if [[ -z "$base" ]]; then
|
||||
base="$(git merge-base origin/main-v2 "$head" 2>/dev/null || git merge-base main-v2 "$head")"
|
||||
fi
|
||||
diff_base="$base"
|
||||
if merge_base="$(git merge-base "$base" "$head" 2>/dev/null)"; then
|
||||
diff_base="$merge_base"
|
||||
fi
|
||||
changed_input="$(git diff --name-only "$diff_base" "$head")"
|
||||
fi
|
||||
|
||||
changed_files=()
|
||||
while IFS= read -r file; do
|
||||
[[ -z "$file" ]] && continue
|
||||
changed_files+=("$file")
|
||||
done <<< "$changed_input"
|
||||
|
||||
cache_sensitive=()
|
||||
system_prompt_sensitive=()
|
||||
|
||||
for file in "${changed_files[@]:-}"; do
|
||||
case "$file" in
|
||||
desktop/session_prompt.go|\
|
||||
internal/agent/agent.go|\
|
||||
internal/agent/ask.go|\
|
||||
internal/agent/cache*|\
|
||||
internal/agent/compact*|\
|
||||
internal/agent/parallel_tasks.go|\
|
||||
internal/agent/prune*|\
|
||||
internal/agent/subagent_registry*|\
|
||||
internal/agent/task.go|\
|
||||
internal/boot/*|\
|
||||
internal/command/slashtool.go|\
|
||||
internal/config/config.go|\
|
||||
internal/config/system_prompt*|\
|
||||
internal/environment/*|\
|
||||
internal/history/tool.go|\
|
||||
internal/installsource/*|\
|
||||
internal/lsp/tool.go|\
|
||||
internal/memory/*|\
|
||||
internal/outputstyle/*|\
|
||||
internal/plugin/*|\
|
||||
internal/provider/*|\
|
||||
internal/skill/*|\
|
||||
internal/tool/*|\
|
||||
scripts/cache-guard.sh|\
|
||||
scripts/check-cache-impact.sh)
|
||||
cache_sensitive+=("$file")
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$file" in
|
||||
desktop/session_prompt.go|\
|
||||
internal/agent/task.go|\
|
||||
internal/boot/*|\
|
||||
internal/config/config.go|\
|
||||
internal/config/system_prompt*|\
|
||||
internal/environment/*|\
|
||||
internal/memory/*|\
|
||||
internal/outputstyle/*|\
|
||||
internal/skill/*)
|
||||
system_prompt_sensitive+=("$file")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "${#cache_sensitive[@]}" -eq 0 ]]; then
|
||||
echo "No cache-sensitive prompt/tool files changed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
failures=()
|
||||
|
||||
trim() {
|
||||
local s="$1"
|
||||
s="${s#"${s%%[![:space:]]*}"}"
|
||||
s="${s%"${s##*[![:space:]]}"}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
field_value() {
|
||||
local label="$1"
|
||||
local line
|
||||
line="$(printf '%s\n' "$body" | grep -Eim1 "^[[:space:]>#*_-]*${label}[[:space:]]*:" || true)"
|
||||
[[ -z "$line" ]] && return 1
|
||||
trim "${line#*:}"
|
||||
}
|
||||
|
||||
require_field() {
|
||||
local label="$1"
|
||||
local value
|
||||
if ! value="$(field_value "$label")"; then
|
||||
failures+=("missing ${label}: line")
|
||||
return
|
||||
fi
|
||||
if [[ -z "$value" || "$value" =~ ^[Tt][Oo][Dd][Oo]($|[[:space:]:-]) || "$value" =~ ^[Tt][Bb][Dd]($|[[:space:]:-]) ]]; then
|
||||
failures+=("${label}: must be filled out")
|
||||
fi
|
||||
}
|
||||
|
||||
require_review_field() {
|
||||
local label="$1"
|
||||
local value
|
||||
if ! value="$(field_value "$label")"; then
|
||||
failures+=("missing ${label}: line")
|
||||
return
|
||||
fi
|
||||
local lower
|
||||
lower="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ -z "$value" || "$lower" =~ ^todo($|[[:space:]:-]) || "$lower" =~ ^tbd($|[[:space:]:-]) || "$lower" =~ ^n/?a($|[[:space:]:-]) || "$lower" =~ ^none($|[[:space:]:-]) ]]; then
|
||||
failures+=("${label}: must name the explicit system-prompt review/approval")
|
||||
fi
|
||||
}
|
||||
|
||||
require_field "Cache-impact"
|
||||
require_field "Cache-guard"
|
||||
|
||||
if [[ "${#system_prompt_sensitive[@]}" -gt 0 ]]; then
|
||||
require_review_field "System-prompt-review"
|
||||
fi
|
||||
|
||||
if [[ "${#failures[@]}" -gt 0 ]]; then
|
||||
{
|
||||
echo "Cache impact check failed."
|
||||
echo
|
||||
echo "Cache-sensitive files changed:"
|
||||
printf ' - %s\n' "${cache_sensitive[@]}"
|
||||
if [[ "${#system_prompt_sensitive[@]}" -gt 0 ]]; then
|
||||
echo
|
||||
echo "System-prompt-sensitive files changed:"
|
||||
printf ' - %s\n' "${system_prompt_sensitive[@]}"
|
||||
fi
|
||||
echo
|
||||
echo "Required PR body lines:"
|
||||
echo " Cache-impact: <none|low|medium|high> - <reason>"
|
||||
echo " Cache-guard: <focused guard test/command or existing guard rationale>"
|
||||
if [[ "${#system_prompt_sensitive[@]}" -gt 0 ]]; then
|
||||
echo " System-prompt-review: <reviewer/approval note>"
|
||||
fi
|
||||
echo
|
||||
echo "Failures:"
|
||||
printf ' - %s\n' "${failures[@]}"
|
||||
} >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Cache impact check passed."
|
||||
echo "Cache-sensitive files:"
|
||||
printf ' - %s\n' "${cache_sensitive[@]}"
|
||||
Executable
+176
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build and package the Wails desktop app for one platform. Wails cannot
|
||||
# cross-compile a CGO+webview binary, so this runs on a native runner per target
|
||||
# (see .github/workflows/release-desktop.yml) and is invoked once per matrix entry.
|
||||
#
|
||||
# Output lands in <repo>/dist/ with stable, platform-keyed names that
|
||||
# desktop/cmd/sign's `manifest` subcommand maps back to update.PlatformKey:
|
||||
# macOS: Reasonix-darwin-<arch>.zip (ditto archive; updater channel)
|
||||
# Reasonix-darwin-universal.dmg (drag-to-install; human download)
|
||||
# Windows: Reasonix-windows-<arch>-installer.exe (NSIS per-user installer; updater channel)
|
||||
# Reasonix-windows-<arch>.zip (portable human download)
|
||||
# Linux: Reasonix-linux-<arch>.tar.gz (bare binary; updater channel)
|
||||
# Reasonix-linux-<arch>.deb (Debian/Ubuntu package; human download)
|
||||
#
|
||||
# Usage: scripts/desktop-build.sh <os/arch> <version> [channel]
|
||||
# e.g. scripts/desktop-build.sh darwin/arm64 v1.1.0
|
||||
# scripts/desktop-build.sh darwin/arm64 v1.5.0-canary.20260608.42 canary
|
||||
set -euo pipefail
|
||||
|
||||
PLATFORM="${1:?usage: desktop-build.sh <os/arch> <version> [channel]}"
|
||||
VERSION="${2:?usage: desktop-build.sh <os/arch> <version> [channel]}"
|
||||
CHANNEL="${3:-stable}"
|
||||
|
||||
os="${PLATFORM%/*}"
|
||||
arch="${PLATFORM#*/}"
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
APPNAME="Reasonix" # wails.json productName -> Reasonix.app
|
||||
BINNAME="reasonix-desktop" # wails.json outputfilename -> linux binary name
|
||||
|
||||
cd "$ROOT/desktop"
|
||||
|
||||
# Stamp the version resource (Windows file properties, macOS CFBundleVersion) from
|
||||
# the tag. Wails feeds info.productVersion into goversioninfo and NSIS's
|
||||
# VIFileVersion, both of which demand a strictly numeric X.X.X, so strip the
|
||||
# leading "v" AND any prerelease suffix (a `-rc1` tag would otherwise abort the
|
||||
# installer build). The full tag still rides in ldflags for the in-app version.
|
||||
numver="${VERSION#v}"; numver="${numver%%-*}"
|
||||
node -e 'const fs=require("fs"),f="wails.json",j=JSON.parse(fs.readFileSync(f,"utf8"));j.info.productVersion=process.argv[1];fs.writeFileSync(f,JSON.stringify(j,null,2)+"\n")' "$numver"
|
||||
|
||||
# NSIS installer is Windows-only (Wails requires a single windows target for -nsis).
|
||||
ldflags="-X main.version=$VERSION -X main.channel=$CHANNEL"
|
||||
[ "$os" = "darwin" ] && [ "${HAS_APPLE_CERT:-}" = "true" ] && ldflags="$ldflags -X main.macSelfUpdate=true"
|
||||
UPDATE_HELPER="reasonix-update-helper.exe"
|
||||
if [ "$os" = windows ]; then
|
||||
echo "==> go build Windows update helper"
|
||||
GOOS=windows GOARCH="$arch" go build -trimpath -ldflags="-s -w" \
|
||||
-o "build/windows/installer/$UPDATE_HELPER" ./cmd/update-helper
|
||||
fi
|
||||
build_args=()
|
||||
[ "${DESKTOP_BUILD_CLEAN:-1}" != "0" ] && build_args+=(-clean)
|
||||
build_args+=(-platform "$PLATFORM" -ldflags "$ldflags")
|
||||
[ "$os" = windows ] && build_args+=(-nsis -webview2 embed)
|
||||
# Link cgo against WebKitGTK 4.1: 4.0 (libwebkit2gtk-4.0.so.37) is gone on
|
||||
# Ubuntu 24.04+/Fedora 40+, while 4.1 ships from Ubuntu 22.04 onward.
|
||||
[ "$os" = linux ] && build_args+=(-tags webkit2_41)
|
||||
|
||||
echo "==> wails build ${build_args[*]}"
|
||||
wails build "${build_args[@]}"
|
||||
|
||||
mkdir -p "$ROOT/dist"
|
||||
|
||||
case "$os" in
|
||||
darwin)
|
||||
# Wails names the bundle after outputfilename (reasonix-desktop.app); repackage
|
||||
# it as Reasonix.app for a clean user-facing name.
|
||||
staging=$(mktemp -d)
|
||||
app="$staging/${APPNAME}.app"
|
||||
cp -R "build/bin/reasonix-desktop.app" "$app"
|
||||
|
||||
# Two signing paths, selected by HAS_APPLE_CERT (set by release-desktop.yml when
|
||||
# the APPLE_* secrets are present). With a real Developer ID cert + notarization
|
||||
# key we sign with a hardened runtime, notarize, and staple — a downloaded build
|
||||
# then opens with no Gatekeeper prompt. Without it we ad-hoc sign as before (still
|
||||
# un-notarized; users clear the quarantine attribute per desktop/README.md). The
|
||||
# fallback keeps fork/local builds working with no secrets configured.
|
||||
if [ "${HAS_APPLE_CERT:-}" = "true" ]; then
|
||||
identity="$(security find-identity -v -p codesigning | awk -F'"' '/Developer ID Application/{print $2; exit}')"
|
||||
[ -n "$identity" ] || { echo "HAS_APPLE_CERT=true but no 'Developer ID Application' identity found in the keychain" >&2; exit 1; }
|
||||
echo "==> codesign (Developer ID): $identity"
|
||||
codesign --force --deep --timestamp --options runtime \
|
||||
--entitlements "$ROOT/desktop/build/darwin/entitlements.plist" \
|
||||
-s "$identity" "$app"
|
||||
# notarytool wants an archive, not a bare bundle: zip the .app, submit, wait,
|
||||
# then staple the ticket back onto the bundle so it verifies offline.
|
||||
ditto -c -k --keepParent "$app" "$staging/notarize.zip"
|
||||
echo "==> notarytool submit (app)"
|
||||
xcrun notarytool submit "$staging/notarize.zip" \
|
||||
--key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY_ID" \
|
||||
--issuer "$APPLE_API_ISSUER_ID" --wait
|
||||
xcrun stapler staple "$app"
|
||||
else
|
||||
# Ad-hoc cuts the "is damaged" error somewhat but is NOT notarized; users may
|
||||
# still need `xattr -dr com.apple.quarantine` (see desktop/README.md).
|
||||
codesign --force --deep -s - "$app"
|
||||
fi
|
||||
|
||||
if [ "$arch" = universal ]; then
|
||||
# One universal .app covers Intel + Apple Silicon; publish it under both
|
||||
# manifest keys so the updater's darwin-arm64/darwin-amd64 lookup finds it
|
||||
# (avoids a scarce macos-13 Intel runner).
|
||||
ditto -c -k --keepParent "$app" "$ROOT/dist/${APPNAME}-darwin-arm64.zip"
|
||||
ditto -c -k --keepParent "$app" "$ROOT/dist/${APPNAME}-darwin-amd64.zip"
|
||||
else
|
||||
ditto -c -k --keepParent "$app" "$ROOT/dist/${APPNAME}-darwin-${arch}.zip"
|
||||
fi
|
||||
if [ "${DESKTOP_BUILD_SKIP_DMG:-0}" = "1" ]; then
|
||||
echo "==> skip DMG packaging (DESKTOP_BUILD_SKIP_DMG=1)"
|
||||
else
|
||||
# A drag-to-Applications .dmg for first-time human download. Named -universal so
|
||||
# cmd/sign's substring match (darwin-arm64/darwin-amd64) skips it: the .zip stays
|
||||
# the updater channel, the .dmg is release-page only. create-dmg can exit nonzero
|
||||
# while still writing the image, so gate on the file existing, not the exit code.
|
||||
dmgsrc=$(mktemp -d)
|
||||
cp -R "$app" "$dmgsrc/${APPNAME}.app"
|
||||
dmg="$ROOT/dist/${APPNAME}-darwin-universal.dmg"
|
||||
create-dmg \
|
||||
--volname "$APPNAME" \
|
||||
--window-size 540 380 \
|
||||
--icon-size 110 \
|
||||
--icon "${APPNAME}.app" 150 190 \
|
||||
--app-drop-link 390 190 \
|
||||
--no-internet-enable \
|
||||
"$dmg" "$dmgsrc" || true
|
||||
[ -f "$dmg" ] || { echo "create-dmg did not produce $dmg" >&2; exit 1; }
|
||||
# The .dmg is a separately-downloaded artifact, so sign + notarize + staple the
|
||||
# disk image itself too — the stapled .app inside isn't enough for the image.
|
||||
if [ "${HAS_APPLE_CERT:-}" = "true" ]; then
|
||||
codesign --force --timestamp -s "$identity" "$dmg"
|
||||
echo "==> notarytool submit (dmg)"
|
||||
xcrun notarytool submit "$dmg" \
|
||||
--key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY_ID" \
|
||||
--issuer "$APPLE_API_ISSUER_ID" --wait
|
||||
xcrun stapler staple "$dmg"
|
||||
fi
|
||||
rm -rf "$dmgsrc"
|
||||
fi
|
||||
rm -rf "$staging"
|
||||
;;
|
||||
windows)
|
||||
# `wails build -nsis` writes the installer under build/bin; its exact name
|
||||
# varies, so glob for it and copy to a stable, platform-keyed name.
|
||||
installer=$(ls build/bin/*installer*.exe 2>/dev/null | head -n1 || true)
|
||||
[ -n "$installer" ] || { echo "no NSIS installer found in build/bin" >&2; exit 1; }
|
||||
cp "$installer" "$ROOT/dist/${APPNAME}-windows-${arch}-installer.exe"
|
||||
portable=$(find build/bin -maxdepth 1 -type f -name "*.exe" ! -name "*installer*.exe" | head -n1 || true)
|
||||
[ -n "$portable" ] || { echo "no portable Windows exe found in build/bin" >&2; exit 1; }
|
||||
staging=$(mktemp -d)
|
||||
cp "$portable" "$staging/${APPNAME}.exe"
|
||||
helper="build/windows/installer/$UPDATE_HELPER"
|
||||
if [ -f "$helper" ]; then
|
||||
cp "$helper" "$staging/$UPDATE_HELPER"
|
||||
fi
|
||||
src_win=$(cygpath -w "$staging/${APPNAME}.exe")
|
||||
zip_win=$(cygpath -w "$ROOT/dist/${APPNAME}-windows-${arch}.zip")
|
||||
powershell.exe -NoProfile -Command "Compress-Archive -Force -LiteralPath '$src_win' -DestinationPath '$zip_win'"
|
||||
rm -rf "$staging"
|
||||
;;
|
||||
linux)
|
||||
tar -czf "$ROOT/dist/${APPNAME}-linux-${arch}.tar.gz" -C build/bin "$BINNAME"
|
||||
# Also build a .deb for Debian/Ubuntu users (goreleaser/nfpm; see
|
||||
# desktop/build/linux/nfpm.yaml). Human-download only: the Linux updater channel
|
||||
# stays the tarball and cmd/sign's manifest skips .deb files. nfpm reads
|
||||
# $DEB_VERSION/$DEB_ARCH — dpkg wants a strict numeric version, so reuse numver.
|
||||
DEB_VERSION="$numver" DEB_ARCH="$arch" \
|
||||
nfpm package --config build/linux/nfpm.yaml --packager deb \
|
||||
--target "$ROOT/dist/${APPNAME}-linux-${arch}.deb"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported os: $os" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "==> packaged into dist/:"
|
||||
ls -la "$ROOT/dist"
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print the slowest tests from Go's JSON test stream."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def events_from(path):
|
||||
stream = sys.stdin if path == "-" else open(path, "r", encoding="utf-8")
|
||||
try:
|
||||
for line in stream:
|
||||
try:
|
||||
yield json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
finally:
|
||||
if stream is not sys.stdin:
|
||||
stream.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Rank test cases from `go test -json` output by elapsed seconds."
|
||||
)
|
||||
parser.add_argument("json_file", nargs="?", default="-", help="JSON stream file, or stdin")
|
||||
parser.add_argument("-n", "--limit", type=int, default=20, help="number of rows to print")
|
||||
parser.add_argument("--min", type=float, default=0.0, help="minimum elapsed seconds")
|
||||
args = parser.parse_args()
|
||||
|
||||
rows = []
|
||||
for event in events_from(args.json_file):
|
||||
if event.get("Action") not in {"pass", "fail", "skip"}:
|
||||
continue
|
||||
test = event.get("Test")
|
||||
elapsed = event.get("Elapsed")
|
||||
if not test or elapsed is None or elapsed < args.min:
|
||||
continue
|
||||
package = event.get("Package", "")
|
||||
name = f"{package}.{test}" if package else test
|
||||
rows.append((float(elapsed), event["Action"], name))
|
||||
|
||||
rows.sort(key=lambda row: row[0], reverse=True)
|
||||
if not rows:
|
||||
print("No test timing rows found.")
|
||||
return
|
||||
|
||||
print(f"{'SECONDS':>8} {'STATE':<5} TEST")
|
||||
for elapsed, action, name in rows[: args.limit]:
|
||||
print(f"{elapsed:8.3f} {action:<5} {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Resolve tag/version/channel/prerelease for one desktop release run, shared by the
|
||||
# build, publish, and mirror jobs so they agree on a single value. Reads the run's
|
||||
# context from env and writes the four outputs to $GITHUB_OUTPUT.
|
||||
#
|
||||
# stable: from a desktop-v* tag push, or a manual dispatch with `tag`.
|
||||
# canary: a manual dispatch with channel=canary; version is synthesized from
|
||||
# base_version + the monotonic run_number, tag is the rolling
|
||||
# `desktop-canary`, and it is always a prerelease.
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${EVENT_NAME:-}" = "workflow_dispatch" ] && [ "${IN_CHANNEL:-stable}" = "canary" ]; then
|
||||
base="${IN_BASE_VERSION:?canary dispatch requires base_version}"
|
||||
base="${base#v}"
|
||||
base="${base#desktop-v}"
|
||||
version="v${base}-canary.${RUN_NUMBER}"
|
||||
tag="desktop-canary"
|
||||
channel="canary"
|
||||
prerelease="true"
|
||||
else
|
||||
if [ "${EVENT_NAME:-}" = "workflow_dispatch" ]; then
|
||||
tag="${IN_TAG:?stable dispatch requires tag}"
|
||||
else
|
||||
tag="${REF_NAME}"
|
||||
fi
|
||||
version="${tag#desktop-}"
|
||||
channel="stable"
|
||||
case "$version" in
|
||||
*-*) prerelease="true" ;;
|
||||
*) prerelease="false" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
{
|
||||
echo "tag=$tag"
|
||||
echo "version=$version"
|
||||
echo "channel=$channel"
|
||||
echo "prerelease=$prerelease"
|
||||
} >>"$GITHUB_OUTPUT"
|
||||
|
||||
echo "resolved: tag=$tag version=$version channel=$channel prerelease=$prerelease"
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
|
||||
const repo = 'esengine/DeepSeek-Reasonix';
|
||||
const api = `https://api.github.com/repos/${repo}/contributors?per_page=20&anon=1`;
|
||||
const startMarker = '<!-- reasonix-top-contributors:start -->';
|
||||
const endMarker = '<!-- reasonix-top-contributors:end -->';
|
||||
|
||||
const headers = {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'reasonix-acknowledgments-updater',
|
||||
};
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
|
||||
const res = await fetch(api, { headers });
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub contributors API failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const contributors = await res.json();
|
||||
if (!Array.isArray(contributors) || contributors.length === 0) {
|
||||
throw new Error('GitHub contributors API returned no contributors');
|
||||
}
|
||||
|
||||
const top = contributors.slice(0, 20).map((c, index) => ({
|
||||
rank: index + 1,
|
||||
login: typeof c.login === 'string' ? c.login : '',
|
||||
name: typeof c.name === 'string' ? c.name : '',
|
||||
url: typeof c.html_url === 'string' ? c.html_url : '',
|
||||
type: typeof c.type === 'string' ? c.type : '',
|
||||
commits: Number(c.contributions) || 0,
|
||||
}));
|
||||
|
||||
await updateReadme('README.md', renderTable(top, 'en'));
|
||||
await updateReadme('README.zh-CN.md', renderTable(top, 'zh'));
|
||||
|
||||
function renderTable(rows, locale) {
|
||||
const header = '| Contributor | Contributor | Contributor | Contributor |\n| --- | --- | --- | --- |';
|
||||
const cells = rows.map((row) => renderContributor(row, locale));
|
||||
const tableRows = [];
|
||||
for (let i = 0; i < cells.length; i += 4) {
|
||||
tableRows.push(`| ${cells.slice(i, i + 4).join(' | ')} |`);
|
||||
}
|
||||
return [
|
||||
startMarker,
|
||||
header,
|
||||
...tableRows,
|
||||
endMarker,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function renderContributor(row, locale) {
|
||||
const label = row.login || row.name || `anonymous-${row.rank}`;
|
||||
const escaped = escapeMarkdown(label);
|
||||
if (row.url) {
|
||||
return `[**${escaped}**](${row.url})`;
|
||||
}
|
||||
const anonymous = locale === 'zh' ? '(anonymous)' : ' (anonymous)';
|
||||
return `**${escaped}**${anonymous}`;
|
||||
}
|
||||
|
||||
async function updateReadme(path, replacement) {
|
||||
const original = await readFile(path, 'utf8');
|
||||
const start = original.indexOf(startMarker);
|
||||
const end = original.indexOf(endMarker);
|
||||
if (start === -1 || end === -1 || end < start) {
|
||||
throw new Error(`${path} is missing ${startMarker}/${endMarker}`);
|
||||
}
|
||||
const next = original.slice(0, start) + replacement + original.slice(end + endMarker.length);
|
||||
if (next !== original) {
|
||||
await writeFile(path, next);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeMarkdown(value) {
|
||||
return String(value).replace(/[\\|[\]]/g, '\\$&');
|
||||
}
|
||||
Reference in New Issue
Block a user