chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:00:08 +08:00
commit 6345505628
1516 changed files with 473769 additions and 0 deletions
+111
View File
@@ -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.`);
+30
View File
@@ -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)
+188
View File
@@ -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[@]}"
+176
View File
@@ -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"
+54
View File
@@ -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()
+41
View File
@@ -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"
+79
View File
@@ -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, '\\$&');
}