chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Every pull request requests review from the maintainers below.
|
||||
# Either maintainer can review and merge any change.
|
||||
* @SivanCola @esengine
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Bug report
|
||||
description: Report a bug in DeepSeek-Reasonix
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for filing a bug. Pick which version line you're on below — it gets
|
||||
labeled (`v1`/`v2`) automatically, no maintainer action needed.
|
||||
- type: dropdown
|
||||
id: version-line
|
||||
attributes:
|
||||
label: Version line
|
||||
description: Which line are you running?
|
||||
options:
|
||||
- "v2 — Go rewrite (1.x), main-v2 (active development)"
|
||||
- "v1 — Legacy TypeScript (0.x), maintenance only"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Exact version
|
||||
description: Output of `reasonix --version`.
|
||||
placeholder: e.g. 1.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: A clear description of the bug, and what you expected instead.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
placeholder: |
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS / platform
|
||||
placeholder: e.g. macOS 15.3 (arm64), Ubuntu 24.04, Windows 11
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant logs or output
|
||||
description: Paste any error output. Automatically formatted as code.
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & discussions
|
||||
url: https://github.com/esengine/DeepSeek-Reasonix/discussions
|
||||
about: For usage questions, ideas, and general discussion — please use Discussions instead of opening an issue.
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea or improvement
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: dropdown
|
||||
id: version-line
|
||||
attributes:
|
||||
label: Version line
|
||||
description: Which line is this for?
|
||||
options:
|
||||
- "v2 — Go rewrite (1.x), main-v2 (active development)"
|
||||
- "v1 — Legacy TypeScript (0.x), maintenance only"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What problem does this solve?
|
||||
description: What are you trying to do, and what gets in the way today?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: How would you like it to work?
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,49 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
go:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/desktop"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
go:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/desktop/frontend"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
npm:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/site"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
npm:
|
||||
patterns: ["*"]
|
||||
update-types: ["minor", "patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
actions:
|
||||
patterns: ["*"]
|
||||
@@ -0,0 +1,51 @@
|
||||
# Area labels auto-applied to PRs by .github/workflows/pr-auto-label.yml
|
||||
# (actions/labeler), based on the changed file paths. Keep the area set in sync
|
||||
# with issue-auto-label.yml. severity/platform are issue-only — a PR is a change,
|
||||
# not a bug report — and v1/v2 are handled by pr-version-label.yml.
|
||||
|
||||
agent:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'internal/agent/**'
|
||||
- 'internal/control/**'
|
||||
|
||||
mcp:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'internal/plugin/**'
|
||||
- 'internal/codegraph/**'
|
||||
|
||||
config:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'internal/config/**'
|
||||
- 'internal/boot/**'
|
||||
|
||||
provider:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'internal/provider/**'
|
||||
|
||||
skills:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'internal/skill/**'
|
||||
- 'internal/tool/**'
|
||||
|
||||
tui:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'internal/cli/**'
|
||||
|
||||
desktop:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'desktop/**'
|
||||
|
||||
updater:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'desktop/cmd/sign/**'
|
||||
- 'desktop/updater*.go'
|
||||
- 'scripts/desktop-build.sh'
|
||||
- '.github/workflows/release*.yml'
|
||||
@@ -0,0 +1,19 @@
|
||||
## Summary
|
||||
|
||||
-
|
||||
|
||||
## Verification
|
||||
|
||||
-
|
||||
|
||||
## Cache impact
|
||||
|
||||
Cache-impact: TODO
|
||||
Cache-guard: TODO
|
||||
System-prompt-review: N/A
|
||||
|
||||
For cache-sensitive changes, fill these lines before requesting review:
|
||||
|
||||
- `Cache-impact`: `none`, `low`, `medium`, or `high`, plus the reason.
|
||||
- `Cache-guard`: the focused guard test/command added or run, or why an existing guard covers the change.
|
||||
- `System-prompt-review`: required reviewer/approval note when provider-visible system prompt, memory prefix, output style, or skill index behavior changes.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
@@ -0,0 +1,37 @@
|
||||
name: Cache impact guard
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [main-v2]
|
||||
types: [opened, synchronize, reopened, edited, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: cache-impact-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
cache-impact:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Collect PR changed files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" \
|
||||
--jq '.[].filename' > "$RUNNER_TEMP/cache-impact-files.txt"
|
||||
|
||||
- name: Check cache-sensitive PR metadata
|
||||
env:
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
CACHE_IMPACT_CHANGED_FILES_FILE: ${{ runner.temp }}/cache-impact-files.txt
|
||||
run: bash scripts/check-cache-impact.sh
|
||||
@@ -0,0 +1,283 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main-v2]
|
||||
pull_request:
|
||||
branches: [main-v2]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
# Skipped on Windows: the runner checks out CRLF, so gofmt -l flags every
|
||||
# file. gofmt output is OS-independent, so the Unix legs already cover it.
|
||||
- name: gofmt
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
# Root module only — desktop/ is a separate module with its own tooling.
|
||||
unformatted=$(gofmt -l . | grep -v '^desktop/' || true)
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "These files are not gofmt-clean:"
|
||||
echo "$unformatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: build
|
||||
run: go build ./...
|
||||
|
||||
- name: test
|
||||
if: runner.os != 'Windows'
|
||||
env:
|
||||
# Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
|
||||
# regression there silently tanks the cache hit rate the project is
|
||||
# built around.
|
||||
REASONIX_RELEASE_CACHE_GUARD: "1"
|
||||
run: go test ./...
|
||||
|
||||
- name: test
|
||||
if: runner.os == 'Windows'
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
# Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
|
||||
# regression there silently tanks the cache hit rate the project is
|
||||
# built around.
|
||||
REASONIX_RELEASE_CACHE_GUARD: "1"
|
||||
# Bound sandbox helper children in Windows tests so a failed OS-level
|
||||
# launch cannot pin the Actions step after Go's package timeout fires.
|
||||
WINDOWS_SANDBOX_WAIT_MS: "20000"
|
||||
run: go test -p 4 -timeout=3m ./...
|
||||
|
||||
race:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
# The matrix never runs -race (it needs cgo); the project's concurrency
|
||||
# (plugin fan-out, background phase B, jobs Kill/Wait) would otherwise
|
||||
# ship without race coverage.
|
||||
- name: test -race
|
||||
env:
|
||||
REASONIX_RELEASE_CACHE_GUARD: "1"
|
||||
run: go test -race ./...
|
||||
|
||||
desktop:
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: desktop/go.mod
|
||||
cache: true
|
||||
cache-dependency-path: desktop/go.sum
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
cache-dependency-path: desktop/frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: |
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
|
||||
|
||||
- name: gofmt
|
||||
run: |
|
||||
unformatted=$(gofmt -l .)
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "These files are not gofmt-clean:"
|
||||
echo "$unformatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: go.mod tidy
|
||||
run: |
|
||||
go mod tidy
|
||||
if ! git diff --quiet -- go.mod go.sum; then
|
||||
echo "desktop/go.mod or go.sum is stale - run 'cd desktop && go mod tidy' and commit."
|
||||
git diff -- go.mod go.sum
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# WebKitGTK 4.0 toolchain (pinned to ubuntu-22.04; no webkit2_41 tag).
|
||||
- name: Install Linux build deps
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.0-dev
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
wails generate module
|
||||
pnpm --dir frontend install --frozen-lockfile
|
||||
pnpm --dir frontend build
|
||||
|
||||
- name: vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.12.2
|
||||
working-directory: desktop
|
||||
args: --timeout=5m
|
||||
|
||||
- name: build
|
||||
run: go build ./...
|
||||
|
||||
- name: test
|
||||
run: go test ./...
|
||||
|
||||
# Desktop project-root matching is case-insensitive only on Windows
|
||||
# (sameDesktopPath folds case when os.PathSeparator is '\'), so the
|
||||
# regression tests for that contract are named *OnWindows and can never
|
||||
# run on the ubuntu leg above.
|
||||
desktop-windows:
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: desktop
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: desktop/go.mod
|
||||
cache: true
|
||||
cache-dependency-path: desktop/go.sum
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
cache-dependency-path: desktop/frontend/pnpm-lock.yaml
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: |
|
||||
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
|
||||
|
||||
# go:embed of frontend/dist needs a built frontend before the package
|
||||
# compiles, same as the ubuntu desktop leg.
|
||||
- name: Build frontend
|
||||
run: |
|
||||
wails generate module
|
||||
pnpm --dir frontend install --frozen-lockfile
|
||||
pnpm --dir frontend build
|
||||
|
||||
- name: test (Windows-only path semantics)
|
||||
timeout-minutes: 10
|
||||
run: go test . -run 'OnWindows$' -v
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.12.2
|
||||
args: --timeout=5m
|
||||
|
||||
# The site/ auth client has security-sensitive redirect-validation logic
|
||||
# (safeNext) covered by node:test unit tests. Those tests use only Node
|
||||
# builtins, so no `npm install` is needed — run them directly on every PR so
|
||||
# a regression in redirect validation fails the build instead of shipping.
|
||||
site:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: site
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: test
|
||||
run: npm test
|
||||
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true # informational — stdlib vulns need a Go patch release
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
||||
- name: govulncheck
|
||||
run: govulncheck ./...
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: test with coverage
|
||||
run: go test -coverprofile=coverage.out -covermode=atomic ./...
|
||||
|
||||
- name: upload coverage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage.out
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,40 @@
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main-v2"]
|
||||
pull_request:
|
||||
branches: ["main-v2"]
|
||||
schedule:
|
||||
- cron: "27 3 * * 1"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
actions: read
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: go
|
||||
build-mode: autobuild
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
- language: actions
|
||||
build-mode: none
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Deploy accounts worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main-v2]
|
||||
paths:
|
||||
- 'workers/accounts/**'
|
||||
- '.github/workflows/deploy-accounts-worker.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-accounts-worker
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
cache-dependency-path: workers/accounts/pnpm-lock.yaml
|
||||
- name: Deploy
|
||||
working-directory: workers/accounts
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
npx wrangler deploy
|
||||
- name: Sync RESEND_API_KEY to the worker
|
||||
working-directory: workers/accounts
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
||||
run: |
|
||||
if [ -n "$RESEND_API_KEY" ]; then
|
||||
printf '%s' "$RESEND_API_KEY" | npx wrangler secret put RESEND_API_KEY
|
||||
else
|
||||
echo "RESEND_API_KEY not set in repo secrets; skipping (worker stays in stub email mode)."
|
||||
fi
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Deploy crash worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main-v2]
|
||||
paths:
|
||||
- 'workers/crash-report/**'
|
||||
- '.github/workflows/deploy-crash-worker.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-crash-worker
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: workers/crash-report/package-lock.json
|
||||
- name: Deploy
|
||||
working-directory: workers/crash-report
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: |
|
||||
npm ci
|
||||
npx wrangler deploy
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Deploy forum worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main-v2]
|
||||
paths:
|
||||
- 'workers/forum/**'
|
||||
- '.github/workflows/deploy-forum-worker.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-forum-worker
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
cache-dependency-path: workers/forum/pnpm-lock.yaml
|
||||
- name: Deploy
|
||||
working-directory: workers/forum
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
npx wrangler deploy
|
||||
@@ -0,0 +1,160 @@
|
||||
name: e2e-bot
|
||||
|
||||
# Comment "/e2e" (fixed suite) or "/e2e diff" (generate tests for the PR's diff)
|
||||
# on a pull request to run the e2e benchmark against the real provider and post a
|
||||
# report back. Gated to trusted authors: the job checks out PR-head code and runs
|
||||
# it with the provider API key, so only the repo owner, members, and collaborators
|
||||
# may trigger it.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: e2e-bot-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
if: >-
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/e2e') &&
|
||||
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
|
||||
runs-on: ubuntu-latest
|
||||
# Running PR-head code with the provider secret is gated twice: the author_
|
||||
# association check above, and this deployment environment. Configure the
|
||||
# `e2e-bot` environment with required reviewers in repo settings to force a
|
||||
# human approval per run (actions/untrusted-checkout-toctou).
|
||||
environment: e2e-bot
|
||||
steps:
|
||||
- name: Acknowledge
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
await github.rest.reactions.createForIssueComment({
|
||||
owner: context.repo.owner, repo: context.repo.repo,
|
||||
comment_id: context.payload.comment.id, content: 'eyes',
|
||||
});
|
||||
|
||||
# Default-branch checkout: this is where the harness (cmd/e2ebench), the
|
||||
# suite, and a run --metrics-capable agent live.
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Build harness + fallback agent from the default branch
|
||||
# Harness (e2ebench) and suite always come from main-v2 so a PR can't weaken
|
||||
# its own grader or tests. The agent is rebuilt from the PR head below; this
|
||||
# main-v2 build is only the fallback for heads that predate `run --metrics`.
|
||||
run: |
|
||||
go build -o "$RUNNER_TEMP/reasonix-base" ./cmd/reasonix
|
||||
go build -o "$RUNNER_TEMP/e2ebench" ./cmd/e2ebench
|
||||
cp -r benchmarks/e2e "$RUNNER_TEMP/suite"
|
||||
|
||||
- name: Check out the PR head
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Pin to the head commit resolved now and check it out detached, not the
|
||||
# mutable PR ref: a force-push mid-run can't swap in different code after
|
||||
# the trusted-author gate passed.
|
||||
run: |
|
||||
SHA=$(gh pr view ${{ github.event.issue.number }} --json headRefOid -q .headRefOid)
|
||||
git fetch -q origin "$SHA"
|
||||
git checkout -q --detach "$SHA"
|
||||
|
||||
- name: Build the agent from the PR head
|
||||
# The whole point of the bot is to drive the PR's code, not main-v2's. Fall
|
||||
# back to the main-v2 build only when the PR head can't yield a
|
||||
# run --metrics-capable binary (build break or predates the flag).
|
||||
id: agent
|
||||
run: |
|
||||
bin="$RUNNER_TEMP/reasonix-base"
|
||||
src="main-v2 fallback (PR head lacks run --metrics)"
|
||||
if go build -o "$RUNNER_TEMP/reasonix-pr" ./cmd/reasonix \
|
||||
&& "$RUNNER_TEMP/reasonix-pr" run -h 2>&1 | grep -q -- '-metrics'; then
|
||||
bin="$RUNNER_TEMP/reasonix-pr"
|
||||
src="PR head ($(git rev-parse --short HEAD))"
|
||||
fi
|
||||
echo "bin=$bin" >> "$GITHUB_OUTPUT"
|
||||
echo "src=$src" >> "$GITHUB_OUTPUT"
|
||||
echo "agent under test: $src"
|
||||
|
||||
- name: Write provider config
|
||||
# User config covers suite tasks (they run in temp dirs); the repo-root copy
|
||||
# covers diff mode (the agent runs in the repo root, where project config wins).
|
||||
run: |
|
||||
mkdir -p ~/.config/reasonix
|
||||
cat > /tmp/reasonix-e2e.toml <<EOF
|
||||
default_model = "e2e"
|
||||
|
||||
[[providers]]
|
||||
name = "e2e"
|
||||
kind = "openai"
|
||||
base_url = "${{ vars.REASONIX_E2E_BASE_URL || 'https://api.deepseek.com' }}"
|
||||
model = "${{ vars.REASONIX_E2E_MODEL || 'deepseek-v4-flash' }}"
|
||||
api_key_env = "DEEPSEEK_API_KEY"
|
||||
context_window = 20000 # small so the suite actually exercises compaction + cache churn
|
||||
|
||||
[providers.price]
|
||||
cache_hit = ${{ vars.REASONIX_E2E_PRICE_CACHE_HIT || '0.02' }}
|
||||
input = ${{ vars.REASONIX_E2E_PRICE_INPUT || '1' }}
|
||||
output = ${{ vars.REASONIX_E2E_PRICE_OUTPUT || '2' }}
|
||||
currency = "${{ vars.REASONIX_E2E_PRICE_CURRENCY || '¥' }}"
|
||||
|
||||
[permissions]
|
||||
mode = "allow"
|
||||
|
||||
[codegraph]
|
||||
enabled = false
|
||||
EOF
|
||||
cp /tmp/reasonix-e2e.toml ~/.config/reasonix/config.toml
|
||||
cp /tmp/reasonix-e2e.toml ./reasonix.toml
|
||||
|
||||
- name: Run e2e
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
if [ -z "$DEEPSEEK_API_KEY" ]; then
|
||||
printf 'missing DEEPSEEK_API_KEY secret\n\nAdd a repository secret named DEEPSEEK_API_KEY to enable the e2e bot.\n' > report.md
|
||||
exit 0
|
||||
fi
|
||||
if printf '%s' "$COMMENT_BODY" | grep -q '/e2e[[:space:]]\+diff'; then
|
||||
ATTEMPTS=$(printf '%s' "$COMMENT_BODY" | sed -nE 's@.*/e2e[[:space:]]+diff[[:space:]]+x([0-9]+).*@\1@p' | head -1)
|
||||
[ -z "$ATTEMPTS" ] && ATTEMPTS=1
|
||||
[ "$ATTEMPTS" -gt 5 ] && ATTEMPTS=5
|
||||
BASE_REF=$(gh pr view ${{ github.event.issue.number }} --json baseRefName -q .baseRefName)
|
||||
git fetch -q origin "$BASE_REF"
|
||||
BASE=$(git merge-base "origin/$BASE_REF" HEAD)
|
||||
"$RUNNER_TEMP/e2ebench" -mode diff -bin "${{ steps.agent.outputs.bin }}" -repo . -base "$BASE" -model e2e -attempts "$ATTEMPTS" -out report.md
|
||||
else
|
||||
"$RUNNER_TEMP/e2ebench" -bin "${{ steps.agent.outputs.bin }}" -suite "$RUNNER_TEMP/suite" -model e2e -out report.md -json report.json -budget 400000
|
||||
fi
|
||||
|
||||
- name: Post report
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TRIGGER_USER: ${{ github.event.comment.user.login }}
|
||||
run: |
|
||||
printf '\n> agent: %s · triggered by @%s\n' "${{ steps.agent.outputs.src }}" "$TRIGGER_USER" >> report.md
|
||||
gh pr comment ${{ github.event.issue.number }} --body-file report.md
|
||||
|
||||
- name: Report failure
|
||||
if: failure()
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh pr comment ${{ github.event.issue.number }} --body "🤖 e2e bot failed — see the [run log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})."
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Auto-label issues
|
||||
|
||||
# Classify new issues into area / platform / severity labels using the DeepSeek
|
||||
# API (the project's own model). The model is constrained to a fixed label set —
|
||||
# anything it returns outside the set is dropped — so it can't invent labels.
|
||||
# Issues it can't place into any area get `needs-triage` for a human to look at.
|
||||
# Version (v1/v2) labels are handled separately by issue-version-label.yml.
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: issue-autolabel-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
classify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
|
||||
with:
|
||||
script: |
|
||||
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 issue = context.payload.issue;
|
||||
const title = issue.title || '';
|
||||
const body = (issue.body || '').slice(0, 4000);
|
||||
|
||||
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, plugins, codegraph',
|
||||
' 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');
|
||||
|
||||
let labels = [];
|
||||
try {
|
||||
const res = await fetch('https://api.deepseek.com/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.DEEPSEEK_API_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) {
|
||||
core.warning(`DeepSeek API ${res.status}: ${await res.text()}`);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const parsed = JSON.parse(data.choices[0].message.content);
|
||||
labels = [...(parsed.area || []), ...(parsed.platform || []), ...(parsed.severity || [])]
|
||||
.filter(l => ALLOWED.has(l));
|
||||
} catch (e) {
|
||||
core.warning(`Classification failed: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!labels.some(l => AREA.includes(l))) labels.push('needs-triage');
|
||||
|
||||
if (labels.length) {
|
||||
await github.rest.issues.addLabels({
|
||||
...context.repo,
|
||||
issue_number: issue.number,
|
||||
labels,
|
||||
});
|
||||
core.info(`Applied: ${labels.join(', ')}`);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Label issue version
|
||||
|
||||
# Issue forms can't let a reporter set a label directly (that needs triage rights),
|
||||
# so the bug/feature forms carry a "Version line" dropdown and this workflow reads
|
||||
# the submitted value and applies the matching v1/v2 label on their behalf.
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: issue-version-${{ github.event.issue.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.issue.body || '';
|
||||
// Pull the "Version line" section out of the rendered issue form.
|
||||
const section = body.split(/^###\s+/m).find(s => /^Version line/i.test(s)) || '';
|
||||
const m = section.match(/\bv([12])\b/i);
|
||||
if (!m) {
|
||||
core.info('No version line found; nothing to label.');
|
||||
return;
|
||||
}
|
||||
const choice = 'v' + m[1];
|
||||
const other = choice === 'v2' ? 'v1' : 'v2';
|
||||
await github.rest.issues.addLabels({
|
||||
...context.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: [choice],
|
||||
});
|
||||
// Keep v1/v2 mutually exclusive in case an edit flipped the choice.
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
...context.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: other,
|
||||
});
|
||||
} catch (e) {
|
||||
core.info(`No ${other} label to remove.`);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Deploy site
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main-v2]
|
||||
paths: ['site/**', '.github/workflows/pages.yml']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
- name: Build
|
||||
working-directory: site
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: site/dist
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deploy.outputs.page_url }}
|
||||
steps:
|
||||
- id: deploy
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Auto-label PRs
|
||||
|
||||
# Apply area labels to PRs based on changed paths (see .github/labeler.yml).
|
||||
# Complements the AI-based issue labeler (issue-auto-label.yml). Version (v1/v2)
|
||||
# labels are handled separately by pr-version-label.yml.
|
||||
#
|
||||
# pull_request_target runs in the base repo context so it has the write token
|
||||
# needed to label PRs from forks; actions/labeler only reads the diff file list
|
||||
# (it never checks out or runs PR code), so this is safe.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-autolabel-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
with:
|
||||
sync-labels: false
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Label PR version
|
||||
|
||||
# Contributors (especially from forks) can't set labels themselves — that needs
|
||||
# triage rights. A PR's version line is fully determined by its base branch, so
|
||||
# this reads base.ref and applies v1/v2 on their behalf. pull_request_target runs
|
||||
# in the base repo's context so it has write access even for fork PRs; it only
|
||||
# reads trusted metadata (the base ref) and never checks out or runs PR code.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened, edited]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-version-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const base = context.payload.pull_request.base.ref;
|
||||
const label = base === 'v1' ? 'v1'
|
||||
: base === 'main-v2' ? 'v2'
|
||||
: null;
|
||||
if (!label) {
|
||||
core.info(`Base branch '${base}' isn't a release line; skipping.`);
|
||||
return;
|
||||
}
|
||||
const other = label === 'v2' ? 'v1' : 'v2';
|
||||
await github.rest.issues.addLabels({
|
||||
...context.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
labels: [label],
|
||||
});
|
||||
// Drop the other line's label if the base was changed after opening.
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
...context.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
name: other,
|
||||
});
|
||||
} catch (e) {
|
||||
core.info(`No ${other} label to remove.`);
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
name: Release desktop
|
||||
|
||||
# Desktop (Wails) release line. Tag namespace `desktop-v<semver>` triggers this —
|
||||
# plain `v<semver>` tags are the CLI release (release.yml) and intentionally do NOT
|
||||
# trigger here (GitHub glob `v*` does not match `desktop-v*`). Bump with:
|
||||
# git tag desktop-vX.Y.Z && git push origin desktop-vX.Y.Z
|
||||
#
|
||||
# Wails cannot cross-compile a CGO/WebKit binary, so build/ fans out to one native
|
||||
# runner per platform. Artifacts are minisign-signed (MINISIGN_* secrets), a
|
||||
# latest.json manifest is generated, and everything is published to a GitHub
|
||||
# release and mirrored to R2 (the updater reads R2 first, then the crash worker
|
||||
# release gateway; stable desktop releases own GitHub's repository-wide "latest").
|
||||
#
|
||||
# The same build/sign/manifest pipeline serves two channels. A `desktop-v*` tag
|
||||
# (or manual stable dispatch) publishes a GitHub release and moves R2's latest/
|
||||
# pointer. A manual `channel: canary` dispatch builds with -X main.channel=canary
|
||||
# and uploads only to R2's canary/ pointer — no GitHub release, so canary never
|
||||
# shows on the releases page and never touches latest/.
|
||||
on:
|
||||
push:
|
||||
tags: ["desktop-v*"]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: "Release channel"
|
||||
type: choice
|
||||
options: [stable, canary]
|
||||
default: stable
|
||||
tag:
|
||||
description: "stable: tag to publish (e.g. desktop-v1.1.0)"
|
||||
required: false
|
||||
type: string
|
||||
base_version:
|
||||
description: "canary: intended next stable version, e.g. 1.5.0"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write # create the release and upload artifacts
|
||||
|
||||
jobs:
|
||||
cache-guard:
|
||||
name: cache hit guard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Cache hit guard
|
||||
run: ./scripts/cache-guard.sh
|
||||
|
||||
build:
|
||||
name: build (${{ matrix.name }})
|
||||
needs: cache-guard
|
||||
permissions:
|
||||
contents: read # checkout only; the publish job holds contents: write
|
||||
actions: read # SignPath reads run details + downloads the unsigned artifact
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# One universal macOS build (Intel + Apple Silicon) on a single arm64
|
||||
# runner — avoids the scarce/slow macos-13 Intel runner.
|
||||
- { runner: macos-14, platform: darwin/universal, name: darwin-universal }
|
||||
- { runner: windows-latest, platform: windows/amd64, name: windows-amd64 }
|
||||
- { runner: windows-11-arm, platform: windows/arm64, name: windows-arm64 }
|
||||
- { runner: ubuntu-22.04, platform: linux/amd64, name: linux-amd64 }
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
# Windows Authenticode signing engages only when the SignPath token is set, so
|
||||
# forks / token-less runs still build (unsigned), mirroring the APPLE_* gate.
|
||||
HAS_SIGNPATH: ${{ secrets.SIGNPATH_API_TOKEN != '' }}
|
||||
defaults:
|
||||
run:
|
||||
shell: bash # desktop-build.sh is bash; windows runners default to pwsh otherwise
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: desktop/go.mod
|
||||
cache: true
|
||||
cache-dependency-path: desktop/go.sum
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 10
|
||||
|
||||
# Linux: WebKitGTK 4.1 toolchain (-tags webkit2_41 in desktop-build.sh).
|
||||
# 4.1 ships from ubuntu-22.04 on and is the only one present on 24.04+/Fedora 40+.
|
||||
- name: Install Linux build deps
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.1-dev
|
||||
|
||||
# Linux: nfpm builds the .deb in desktop-build.sh's linux branch. go install
|
||||
# drops it in ~/go/bin, already on PATH (same place the wails CLI lands below).
|
||||
- name: Install nfpm
|
||||
if: runner.os == 'Linux'
|
||||
run: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.46.3
|
||||
|
||||
# Windows: NSIS provides makensis for `wails build -nsis`.
|
||||
- name: Install NSIS
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
choco install nsis -y --no-progress
|
||||
echo "C:\Program Files (x86)\NSIS" >> "$GITHUB_PATH"
|
||||
|
||||
# macOS: create-dmg packages the .app into a drag-to-Applications .dmg.
|
||||
- name: Install create-dmg
|
||||
if: runner.os == 'macOS'
|
||||
run: brew install create-dmg
|
||||
|
||||
# macOS signing: import the Developer ID cert into a throwaway keychain and
|
||||
# stage the notarization key. No-ops (and the build ad-hoc signs) when the
|
||||
# APPLE_* secrets aren't set, so forks still build.
|
||||
- name: Import Apple signing certificate
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
APPLE_CERT_P12: ${{ secrets.APPLE_CERT_P12 }}
|
||||
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
|
||||
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||
run: |
|
||||
if [ -z "$APPLE_CERT_P12" ]; then
|
||||
echo "APPLE_CERT_P12 unset — desktop build will ad-hoc sign (un-notarized)"
|
||||
exit 0
|
||||
fi
|
||||
KEYCHAIN="$RUNNER_TEMP/signing.keychain-db"
|
||||
KEYCHAIN_PASS="$(uuidgen)"
|
||||
security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN"
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN"
|
||||
echo "$APPLE_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
|
||||
security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" >/dev/null
|
||||
# Prepend the signing keychain to the search list so codesign / find-identity see it.
|
||||
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
|
||||
echo "$APPLE_API_KEY_P8" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
||||
rm -f "$RUNNER_TEMP/cert.p12"
|
||||
|
||||
- name: Install Wails CLI
|
||||
run: go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: bash scripts/resolve-desktop-release.sh
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
IN_CHANNEL: ${{ inputs.channel }}
|
||||
IN_TAG: ${{ inputs.tag }}
|
||||
IN_BASE_VERSION: ${{ inputs.base_version }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
|
||||
- name: Build and package
|
||||
env:
|
||||
# macOS Developer ID + notarization path turns on only when all five
|
||||
# APPLE_* secrets are present; otherwise desktop-build.sh ad-hoc signs.
|
||||
# Harmless on Windows/Linux runners (only the darwin branch reads these).
|
||||
HAS_APPLE_CERT: ${{ secrets.APPLE_CERT_P12 != '' && secrets.APPLE_CERT_PASSWORD != '' && secrets.APPLE_API_KEY_P8 != '' && secrets.APPLE_API_KEY_ID != '' && secrets.APPLE_API_ISSUER_ID != '' }}
|
||||
APPLE_API_KEY_PATH: ${{ runner.temp }}/notary.p8
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
|
||||
run: scripts/desktop-build.sh "${{ matrix.platform }}" "${{ steps.ver.outputs.version }}" "${{ steps.ver.outputs.channel }}"
|
||||
|
||||
# Authenticode-sign the Windows installer through SignPath. Runs BEFORE minisign:
|
||||
# the signature rewrites the installer's bytes, so the updater's minisign sig and
|
||||
# the manifest SHA must be computed over the signed build. SignPath pulls the file
|
||||
# from the Actions artifact API, so it must be uploaded first. Canary signs with the
|
||||
# test certificate; stable/rc sign with the Foundation release certificate, but only
|
||||
# once SIGNPATH_RELEASE_SIGNING_READY is set — until that cert clears CSR-pending,
|
||||
# stable ships unsigned instead of failing the whole release on an invalid policy.
|
||||
- name: Upload unsigned installer for SignPath
|
||||
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true' && (steps.ver.outputs.channel == 'canary' || vars.SIGNPATH_RELEASE_SIGNING_READY == 'true')
|
||||
id: unsigned-installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unsigned-${{ matrix.name }}
|
||||
path: dist/*installer*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Submit installer for Authenticode signing
|
||||
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true' && (steps.ver.outputs.channel == 'canary' || vars.SIGNPATH_RELEASE_SIGNING_READY == 'true')
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: DeepSeek-Reasonix
|
||||
signing-policy-slug: ${{ steps.ver.outputs.channel == 'canary' && 'test-signing' || 'release-signing' }}
|
||||
artifact-configuration-slug: windows-installer
|
||||
github-artifact-id: ${{ steps.unsigned-installer.outputs.artifact-id }}
|
||||
github-token: ${{ github.token }}
|
||||
wait-for-completion: true
|
||||
# The policy uses an approval process — leave headroom for a human to approve
|
||||
# the request (the approver gets an email on submit) before the job gives up.
|
||||
wait-for-completion-timeout-in-seconds: 1800
|
||||
output-artifact-directory: signed
|
||||
|
||||
- name: Replace installer with signed build
|
||||
if: runner.os == 'Windows' && env.HAS_SIGNPATH == 'true' && (steps.ver.outputs.channel == 'canary' || vars.SIGNPATH_RELEASE_SIGNING_READY == 'true')
|
||||
run: cp signed/*installer*.exe dist/
|
||||
|
||||
- name: Sign artifacts (minisign)
|
||||
working-directory: desktop
|
||||
env:
|
||||
MINISIGN_PRIVATE_KEY: ${{ secrets.MINISIGN_PRIVATE_KEY }}
|
||||
MINISIGN_PASSWORD: ${{ secrets.MINISIGN_PASSWORD }}
|
||||
run: go run ./cmd/sign sign ../dist/*
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dist-${{ matrix.name }}
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: publish release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# Stable publishes go through the `release` environment (esengine must
|
||||
# approve); canary uses the open `canary` environment so maintainers self-serve.
|
||||
environment: ${{ (github.event_name == 'workflow_dispatch' && inputs.channel == 'canary') && 'canary' || 'release' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: desktop/go.mod
|
||||
cache: true
|
||||
cache-dependency-path: desktop/go.sum
|
||||
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: dist
|
||||
pattern: dist-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: bash scripts/resolve-desktop-release.sh
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
IN_CHANNEL: ${{ inputs.channel }}
|
||||
IN_TAG: ${{ inputs.tag }}
|
||||
IN_BASE_VERSION: ${{ inputs.base_version }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
|
||||
# Generate latest.json with GitHub release download URLs; the mirror step
|
||||
# rewrites them to R2 afterwards. GITHUB_REPOSITORY is provided by the runner.
|
||||
- name: Generate manifest
|
||||
working-directory: desktop
|
||||
run: go run ./cmd/sign manifest ../dist "${{ steps.ver.outputs.version }}" "${{ steps.ver.outputs.tag }}"
|
||||
|
||||
# Canary never appears on the GitHub releases page; the mirror job picks up
|
||||
# the signed dist via the canary-dist artifact below. Stable publishes a
|
||||
# GitHub release as usual.
|
||||
- name: Publish GitHub release
|
||||
if: steps.ver.outputs.channel != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Keep the repository homepage focused on the installable desktop app;
|
||||
# the CLI release line is configured not to claim repository-wide latest.
|
||||
args=(--title "Reasonix Desktop ${{ steps.ver.outputs.version }}" --generate-notes)
|
||||
if [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then
|
||||
args+=(--prerelease --latest=false)
|
||||
else
|
||||
args+=(--latest)
|
||||
fi
|
||||
gh release create "${{ steps.ver.outputs.tag }}" dist/* "${args[@]}"
|
||||
|
||||
- name: Upload canary dist for mirror
|
||||
if: steps.ver.outputs.channel == 'canary'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: canary-dist
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
|
||||
mirror:
|
||||
name: mirror to R2
|
||||
needs: publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # gh release download + compatibility manifest upload
|
||||
actions: write # dispatch pages.yml to re-bake the site version
|
||||
# Skip cleanly when R2 isn't configured; GitHub release still works as fallback.
|
||||
if: ${{ github.repository_owner == 'esengine' }}
|
||||
env:
|
||||
HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: bash scripts/resolve-desktop-release.sh
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
IN_CHANNEL: ${{ inputs.channel }}
|
||||
IN_TAG: ${{ inputs.tag }}
|
||||
IN_BASE_VERSION: ${{ inputs.base_version }}
|
||||
REF_NAME: ${{ github.ref_name }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
|
||||
# Canary has no GitHub release — pull the signed dist from the workflow
|
||||
# artifact. Stable pulls from the published release.
|
||||
- name: Download canary dist
|
||||
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel == 'canary'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: canary-dist
|
||||
path: assets
|
||||
|
||||
- name: Download release assets
|
||||
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel != 'canary'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
mkdir -p assets
|
||||
gh release download "${{ steps.ver.outputs.tag }}" -R "${{ github.repository }}" -D assets
|
||||
|
||||
# Rewrite both url and sig inside latest.json from github.com to the R2 CDN,
|
||||
# so the updater pulls the manifest AND the heavy artifacts from R2.
|
||||
- name: Rewrite latest.json URLs to R2
|
||||
if: env.HAS_R2 == 'true'
|
||||
env:
|
||||
R2_PUBLIC_BASE: https://dl.reasonix.io
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
f=assets/latest.json
|
||||
jq --arg base "$R2_PUBLIC_BASE" --arg tag "$TAG" '
|
||||
.platforms |= with_entries(
|
||||
.value.url |= sub("https://github.com/[^/]+/[^/]+/releases/download/[^/]+/"; "\($base)/\($tag)/")
|
||||
| .value.sig |= sub("https://github.com/[^/]+/[^/]+/releases/download/[^/]+/"; "\($base)/\($tag)/")
|
||||
)
|
||||
' "$f" > "$f.new"
|
||||
mv "$f.new" "$f"
|
||||
cat "$f"
|
||||
|
||||
- name: Configure AWS CLI for R2
|
||||
if: env.HAS_R2 == 'true'
|
||||
run: |
|
||||
aws configure set aws_access_key_id "${{ secrets.R2_ACCESS_KEY_ID }}"
|
||||
aws configure set aws_secret_access_key "${{ secrets.R2_SECRET_ACCESS_KEY }}"
|
||||
aws configure set region auto
|
||||
|
||||
- name: Mirror to R2
|
||||
if: env.HAS_R2 == 'true'
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
|
||||
aws s3 cp assets/ "s3://${R2_BUCKET}/${TAG}/" --recursive --endpoint-url "$ENDPOINT"
|
||||
# The pointer a release moves is its channel's alone: canary writes
|
||||
# canary/, stable writes latest/. A stable prerelease (rc) moves neither.
|
||||
if [ "${{ steps.ver.outputs.channel }}" = "canary" ]; then
|
||||
aws s3 cp assets/ "s3://${R2_BUCKET}/canary/" --recursive --endpoint-url "$ENDPOINT"
|
||||
elif [ "${{ steps.ver.outputs.prerelease }}" = "true" ]; then
|
||||
echo "prerelease — leaving R2 latest/ untouched"
|
||||
else
|
||||
aws s3 cp assets/ "s3://${R2_BUCKET}/latest/" --recursive --endpoint-url "$ENDPOINT"
|
||||
fi
|
||||
|
||||
# dl.reasonix.io serves 403 to GitHub Actions egress IPs (Cloudflare bot
|
||||
# protection), so smoke the mirrored objects over the authenticated S3 API
|
||||
# instead of the public edge. This verifies the mirror landed; the public
|
||||
# edge itself is not reachable from CI and is covered by end users' traffic.
|
||||
- name: Smoke desktop release pointers
|
||||
if: env.HAS_R2 == 'true'
|
||||
env:
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
CHANNEL: ${{ steps.ver.outputs.channel }}
|
||||
PRERELEASE: ${{ steps.ver.outputs.prerelease }}
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
|
||||
f=assets/latest.json
|
||||
jq -e --arg version "$VERSION" --arg prefix "https://dl.reasonix.io/${TAG}/" '
|
||||
.version == $version and
|
||||
.download_page == "https://reasonix.io/#start" and
|
||||
([.platforms[] | (.url, .sig)] |
|
||||
all(type == "string" and startswith($prefix) and (contains("/releases/latest/") | not)))
|
||||
' "$f" >/dev/null
|
||||
|
||||
aws s3 cp "s3://${R2_BUCKET}/${TAG}/latest.json" /tmp/reasonix-desktop-tag-latest.json --endpoint-url "$ENDPOINT"
|
||||
jq -e --arg version "$VERSION" '.version == $version' /tmp/reasonix-desktop-tag-latest.json >/dev/null
|
||||
if [ "$CHANNEL" = "canary" ]; then
|
||||
pointer="canary"
|
||||
elif [ "$PRERELEASE" = "true" ]; then
|
||||
pointer=""
|
||||
else
|
||||
pointer="latest"
|
||||
fi
|
||||
if [ -n "$pointer" ]; then
|
||||
aws s3 cp "s3://${R2_BUCKET}/${pointer}/latest.json" /tmp/reasonix-desktop-pointer-latest.json --endpoint-url "$ENDPOINT"
|
||||
jq -e --arg version "$VERSION" '.version == $version' /tmp/reasonix-desktop-pointer-latest.json >/dev/null
|
||||
fi
|
||||
|
||||
jq -r '.platforms[] | .url, .sig' "$f" | while IFS= read -r asset; do
|
||||
key="${asset#https://dl.reasonix.io/}"
|
||||
aws s3api head-object --bucket "$R2_BUCKET" --key "$key" --endpoint-url "$ENDPOINT" >/dev/null
|
||||
done
|
||||
|
||||
# Best-effort probe of the release gateway — the updater's second
|
||||
# manifest source — over the same public edge and Go client UA end users
|
||||
# hit. A 403 here is the known Cloudflare bot-protection gap (#6005:
|
||||
# datacenter/proxy egress gets blocked before the worker runs) and must
|
||||
# not fail the release until a WAF skip rule for /v1/desktop/releases/*
|
||||
# lands; it is surfaced as a warning so the run shows whether the edge
|
||||
# is open. Anything else unexpected (404, 5xx, wrong version) means the
|
||||
# gateway route or pointer regressed and fails hard.
|
||||
- name: Probe public release gateway
|
||||
if: env.HAS_R2 == 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
CHANNEL: ${{ steps.ver.outputs.channel }}
|
||||
PRERELEASE: ${{ steps.ver.outputs.prerelease }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$PRERELEASE" = "true" ]; then
|
||||
echo "prerelease — no pointer moved; skipping gateway probe"
|
||||
exit 0
|
||||
fi
|
||||
chan="stable"
|
||||
[ "$CHANNEL" = "canary" ] && chan="canary"
|
||||
url="https://crash.reasonix.io/v1/desktop/releases/${chan}/latest.json"
|
||||
# curl already prints 000 for a transport failure; || true keeps -e
|
||||
# from killing the step so the case below can route it.
|
||||
code="$(curl -sS -A "Go-http-client/2.0" -o /tmp/gateway-latest.json -w '%{http_code}' "$url" || true)"
|
||||
case "$code" in
|
||||
200)
|
||||
if jq -e --arg version "$VERSION" '.version == $version' /tmp/gateway-latest.json >/dev/null; then
|
||||
echo "gateway serves $VERSION on $chan"
|
||||
else
|
||||
echo "::error::gateway responded 200 but serves $(jq -r '.version // "<none>"' /tmp/gateway-latest.json), want $VERSION — stale or wrong pointer"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
403)
|
||||
echo "::warning::gateway returned 403 to CI egress — known bot-protection gap (#6005), not failing the release"
|
||||
;;
|
||||
000|"")
|
||||
echo "::warning::gateway unreachable from CI (transport error), not failing the release"
|
||||
;;
|
||||
*)
|
||||
echo "::error::gateway returned $code for $url — route or pointer regression"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Attach desktop manifest to matching CLI release
|
||||
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel != 'canary' && steps.ver.outputs.prerelease != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VERSION: ${{ steps.ver.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if gh release view "$VERSION" >/dev/null 2>&1; then
|
||||
gh release upload "$VERSION" assets/latest.json --clobber
|
||||
else
|
||||
echo "CLI release $VERSION does not exist yet; release.yml will attach the compatibility latest.json when it publishes."
|
||||
fi
|
||||
|
||||
# Stable release moved R2 latest/ — rebuild the site so its build-time baked
|
||||
# version + JSON-LD follow (site.js's runtime .rxv refresh can't touch first paint / SEO).
|
||||
- name: Refresh site to the new version
|
||||
if: env.HAS_R2 == 'true' && steps.ver.outputs.channel != 'canary' && steps.ver.outputs.prerelease != 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh workflow run pages.yml --ref main-v2
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Release npm
|
||||
|
||||
# npm line. Tag namespace `npm-v<semver>` triggers this — plain `v<semver>` tags
|
||||
# are the CLI binary + Homebrew release (release.yml) and intentionally do NOT
|
||||
# trigger here (GitHub glob `npm-v*` does not match `v*`). Splitting npm onto its
|
||||
# own tag lets a prerelease ship to npm's `next` channel without touching the
|
||||
# stable Homebrew cask (and lets a stable cask ship without forcing npm to leave
|
||||
# its rc). Bump with:
|
||||
# git tag npm-vX.Y.Z # stable -> publishes under `latest`
|
||||
# git tag npm-vX.Y.Z-rc.N # prerelease -> publishes under `next`
|
||||
# git push origin <tag>
|
||||
#
|
||||
# A manual workflow_dispatch can publish either an opt-in canary from the current
|
||||
# ref or an approved stable/next version when tag creation is restricted. Canary
|
||||
# publishes to the `canary` dist-tag (npm i reasonix@canary) and never moves
|
||||
# `next`/`latest`. build.mjs decides the dist-tag: a `-canary.` build is `canary`,
|
||||
# any other prerelease is `next`, and a stable version is `latest` (#5822 — the
|
||||
# old "promote latest by hand" rule left it pinned at 0.53.2 and downgraded
|
||||
# `npm update -g` users). The verify step below asserts the tag actually landed.
|
||||
on:
|
||||
push:
|
||||
tags: ['npm-v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: "Publish channel"
|
||||
required: true
|
||||
default: canary
|
||||
type: choice
|
||||
options:
|
||||
- canary
|
||||
- stable
|
||||
base_version:
|
||||
description: "Version to publish. Canary appends -canary.<run_number>; stable publishes exactly this version."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cache-guard:
|
||||
name: cache hit guard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- run: ./scripts/cache-guard.sh
|
||||
|
||||
npm:
|
||||
name: publish npm packages
|
||||
needs: cache-guard
|
||||
runs-on: ubuntu-latest
|
||||
# A tag push (npm-v*) or manual stable dispatch publishes a stable/next
|
||||
# release and goes through the `release` environment (esengine approves);
|
||||
# canary dispatches run free.
|
||||
environment: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'canary' && 'canary' || 'release' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
# Tag push uses the npm-v* tag. A canary dispatch synthesizes a
|
||||
# <base>-canary.<run_number> version; a manual stable dispatch publishes
|
||||
# the requested semver exactly. build.mjs strips the leading `npm-`/`v`.
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
base="${{ inputs.base_version }}"; base="${base#v}"
|
||||
if [ "${{ inputs.channel }}" = "canary" ]; then
|
||||
echo "arg=v${base}-canary.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "arg=v${base}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "arg=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- run: node npm/build.mjs "${{ steps.ver.outputs.arg }}" --publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# Assert the dist-tag landed where build.mjs aimed it. This is the guard
|
||||
# that #5822 lacked: `latest` sat on 0.53.2 for months while stables went
|
||||
# to `next`, and nothing noticed until users got downgraded. The registry
|
||||
# applies tags asynchronously after publish, hence the poll.
|
||||
- name: Verify dist-tags
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${{ steps.ver.outputs.arg }}"
|
||||
version="${version#npm-}"; version="${version#v}"
|
||||
case "$version" in
|
||||
*-canary.*) tag=canary ;;
|
||||
*-*) tag=next ;;
|
||||
*) tag=latest ;;
|
||||
esac
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
got="$(npm view reasonix dist-tags."$tag" 2>/dev/null || true)"
|
||||
if [ "$got" = "$version" ]; then
|
||||
echo "dist-tag $tag -> $got"
|
||||
exit 0
|
||||
fi
|
||||
echo "dist-tag $tag -> ${got:-<unset>}, want $version (attempt $attempt)"
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::npm dist-tag '$tag' never landed on $version"
|
||||
exit 1
|
||||
@@ -0,0 +1,173 @@
|
||||
name: Release
|
||||
|
||||
# CLI binary line. Tag namespace `v<semver>` triggers this — it builds the
|
||||
# release archives + checksums, publishes the GitHub release, and updates the
|
||||
# Homebrew cask. npm is a SEPARATE line (release-npm.yml, `npm-v*`) so an npm
|
||||
# prerelease can ship without forcing the stable cask, and vice versa; desktop is
|
||||
# `desktop-v*` (release-desktop.yml). A prerelease `v*-rc*` still builds archives
|
||||
# and a prerelease GitHub release, but goreleaser auto-skips the cask upload
|
||||
# (brew has no prerelease channel — see .goreleaser.yaml).
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write # create the release and upload archives
|
||||
|
||||
jobs:
|
||||
cache-guard:
|
||||
name: cache hit guard
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- run: ./scripts/cache-guard.sh
|
||||
|
||||
goreleaser:
|
||||
name: archives + checksums + homebrew tap
|
||||
needs: cache-guard
|
||||
runs-on: ubuntu-latest
|
||||
# CLI stable release (archives + GitHub release + Homebrew cask). It does not
|
||||
# claim GitHub's repository-wide Latest badge; that belongs to desktop so the
|
||||
# repository homepage points users at the installable app. Gated on the
|
||||
# `release` environment so only esengine can approve it going public.
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
|
||||
- name: Attach desktop manifest compatibility asset
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }}
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$TAG" in
|
||||
*-*)
|
||||
echo "prerelease $TAG — GitHub latest does not move here; skipping desktop manifest compatibility asset"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
if [ "$HAS_R2" != "true" ]; then
|
||||
echo "R2 secrets not configured; skipping desktop manifest compatibility asset"
|
||||
exit 0
|
||||
fi
|
||||
# dl.reasonix.io serves 403 to GitHub Actions egress IPs (Cloudflare bot
|
||||
# protection), so read the manifest over the authenticated S3 API instead
|
||||
# of the public edge.
|
||||
aws configure set aws_access_key_id "${{ secrets.R2_ACCESS_KEY_ID }}"
|
||||
aws configure set aws_secret_access_key "${{ secrets.R2_SECRET_ACCESS_KEY }}"
|
||||
aws configure set region auto
|
||||
aws s3 cp "s3://${R2_BUCKET}/latest/latest.json" latest.raw.json \
|
||||
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
|
||||
jq '.download_page = "https://reasonix.io/#start"' latest.raw.json > latest.json
|
||||
jq -e '
|
||||
([.platforms[] | (.url, .sig)] |
|
||||
all(type == "string" and startswith("https://dl.reasonix.io/") and (contains("/releases/latest/") | not)))
|
||||
' latest.json >/dev/null
|
||||
gh release upload "$TAG" latest.json --clobber
|
||||
|
||||
# The compatibility asset exists for pre-v1.16 desktop updaters that
|
||||
# still poll GitHub's repository-wide latest URL. Desktop releases now
|
||||
# own that Latest badge, but this check still exercises the public fallback
|
||||
# path exactly the way those clients fetch it: anonymously, over the public
|
||||
# edge, with a Go client UA. Unlike dl.reasonix.io (whose bot protection
|
||||
# 403s Actions egress — see the R2 note above), GitHub serves its own
|
||||
# runners, so this can hard-fail. #5826/#5858 shipped a broken update check
|
||||
# for weeks precisely because nothing exercised the public path. Retries
|
||||
# cover the release CDN propagating the freshly uploaded asset.
|
||||
- name: Smoke public compatibility manifest
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$TAG" in
|
||||
*-*)
|
||||
echo "prerelease $TAG — no compatibility asset uploaded; skipping"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
if [ "$HAS_R2" != "true" ]; then
|
||||
echo "R2 secrets not configured; no compatibility asset uploaded; skipping"
|
||||
exit 0
|
||||
fi
|
||||
url="https://github.com/${{ github.repository }}/releases/latest/download/latest.json"
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
if curl -fsSL -A "Go-http-client/2.0" -o /tmp/compat-latest.json "$url"; then
|
||||
jq -e '(.version | type == "string") and (.platforms | type == "object")' /tmp/compat-latest.json >/dev/null
|
||||
echo "public compatibility manifest OK (desktop version $(jq -r .version /tmp/compat-latest.json))"
|
||||
exit 0
|
||||
fi
|
||||
echo "attempt $attempt failed; retrying in 10s"
|
||||
sleep 10
|
||||
done
|
||||
echo "::error::public compatibility manifest unreachable at $url"
|
||||
exit 1
|
||||
|
||||
# A stable CLI release must never leave the npm line behind: v1.17.5
|
||||
# shipped as binaries/Homebrew while npm `latest` still pointed at 0.53.2
|
||||
# (#5822) — every `npm update -g` user was silently downgraded to a
|
||||
# months-old version, and nothing noticed because the npm line
|
||||
# (release-npm.yml, `npm-vX.Y.Z` tags) is triggered independently and the
|
||||
# stable npm tag was simply never pushed. release-npm.yml's own verify
|
||||
# step only guards runs that happen; this guard catches the run that
|
||||
# DIDN'T.
|
||||
#
|
||||
# Two distinct states, two responses (the runs race: both workflows wait
|
||||
# on the release environment independently, and npm dist-tags propagate
|
||||
# asynchronously, so "tag pushed but latest not moved yet" is a NORMAL
|
||||
# mid-release state, not a failure):
|
||||
# - npm-v<version> tag missing -> hard fail. This is the #5822 gap:
|
||||
# nobody pushed the npm release at all.
|
||||
# - tag pushed, latest lagging -> poll briefly, then WARN and pass.
|
||||
# The npm run may still be awaiting its own environment approval;
|
||||
# release-npm.yml's verify step owns asserting the dist-tag lands.
|
||||
- name: Check npm latest dist-tag freshness
|
||||
env:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
case "$TAG" in
|
||||
*-*)
|
||||
echo "prerelease $TAG — npm latest does not move on prereleases; skipping"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
version="${TAG#v}"
|
||||
if ! git ls-remote --exit-code origin "refs/tags/npm-v$version" >/dev/null; then
|
||||
echo "::error::the npm-v$version tag was never pushed — the npm channel is being left behind and 'npm update -g' users will be downgraded to the old 'latest'. Push it: git tag npm-v$version ${TAG} && git push origin npm-v$version (or 'npm dist-tag add reasonix@$version latest' for an already-published version)."
|
||||
exit 1
|
||||
fi
|
||||
for attempt in 1 2 3 4 5 6; do
|
||||
got="$(npm view reasonix dist-tags.latest 2>/dev/null || true)"
|
||||
if [ -n "$got" ]; then
|
||||
newest="$(printf '%s\n%s\n' "$got" "$version" | sort -V | tail -1)"
|
||||
if [ "$newest" = "$got" ]; then
|
||||
echo "npm latest -> $got (>= $version) OK"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "npm latest -> ${got:-<unreadable>}, want >= $version (attempt $attempt)"
|
||||
sleep 10
|
||||
done
|
||||
echo "::warning::npm-v$version is pushed but npm 'latest' is still ${got:-<unreadable>} — the npm publish run is likely awaiting release-environment approval or still propagating. Approve/monitor the release-npm run; its verify step asserts the dist-tag lands."
|
||||
exit 0
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Update acknowledgments
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Update README contributor tables
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: node scripts/update-acknowledgments.mjs
|
||||
- name: Open pull request
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: chore/update-acknowledgments
|
||||
delete-branch: true
|
||||
title: 'Update acknowledgments contributors / 更新致谢贡献者'
|
||||
commit-message: 'docs: update acknowledgments contributors'
|
||||
body: |
|
||||
Refreshes the README acknowledgments tables from the GitHub contributors API.
|
||||
|
||||
- Source: `repos/esengine/DeepSeek-Reasonix/contributors?per_page=20&anon=1`
|
||||
- Anonymous contributors are shown without email addresses.
|
||||
labels: documentation
|
||||
Reference in New Issue
Block a user