refactor: harden tests

This commit is contained in:
zhom
2026-07-21 23:13:17 +04:00
parent f7daf68b52
commit 9624ec846d
81 changed files with 11771 additions and 983 deletions
+6 -1
View File
@@ -2,6 +2,11 @@ name: Bug Report
description: Something isn't working
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Do not include passwords, access tokens, proxy credentials, personal information, or other secrets. Automated triage sends the issue title and body to OpenRouter after removing the logs/screenshots field and redacting common sensitive-data patterns.
- type: textarea
id: description
attributes:
@@ -54,7 +59,7 @@ body:
id: logs
attributes:
label: Error logs or screenshots
description: Run from terminal to get logs. Paste errors, screenshots, or screen recordings.
description: Use Settings → Advanced → Copy logs for a redacted log bundle. Review it before posting. Never include credentials or personal information.
placeholder: Paste logs here or drag screenshots
validations:
required: false
@@ -2,6 +2,11 @@ name: Feature Request
description: Suggest a new feature
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Do not include passwords, access tokens, personal information, or other secrets. Automated triage sends the issue title and body to OpenRouter after redacting common sensitive-data patterns.
- type: textarea
id: description
attributes:
+105 -15
View File
@@ -8,6 +8,19 @@ on:
required: false
default: true
type: boolean
secrets:
TAURI_WEBDRIVER_TOKEN:
description: Read-only token for the test-driver repository
required: false
WAYFERN_TEST_TOKEN:
description: Token used by the full Wayfern integration suite
required: false
RESIDENTIAL_PROXY_URL_ONE_SOCKS:
description: SOCKS5 residential proxy used by the network suite
required: false
RESIDENTIAL_PROXY_URL_ONE_HTTP:
description: HTTP residential proxy used by the network suite
required: false
workflow_dispatch:
inputs:
suite:
@@ -20,6 +33,7 @@ on:
- smoke
- ui
- entities
- network
- integrations
- sync
- browser
@@ -51,13 +65,15 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: donutbrowser
persist-credentials: false
- name: Checkout private cross-platform WebDriver
- name: Checkout test driver
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY || 'zhom/tauri-cross-platform-webdriver' }}
repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY }}
token: ${{ secrets.TAURI_WEBDRIVER_TOKEN || github.token }}
path: tauri-cross-platform-webdriver
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -81,6 +97,7 @@ jobs:
with:
workspaces: |
donutbrowser/src-tauri -> target
donutbrowser/e2e/app -> target
tauri-cross-platform-webdriver -> target
- name: Install Tauri dependencies
@@ -95,8 +112,6 @@ jobs:
- name: Run isolated smoke suite
working-directory: donutbrowser
env:
DONUT_E2E_KEEP_ARTIFACTS: "1"
shell: bash
run: |
if [[ "${RUNNER_OS}" == "Linux" ]]; then
@@ -106,17 +121,17 @@ jobs:
fi
- name: Upload E2E diagnostics
if: always()
uses: actions/upload-artifact@v7
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: app-e2e-smoke-${{ matrix.os }}
path: ${{ runner.temp }}/donut-e2e-*
path: ${{ runner.temp }}/donut-e2e-*/diagnostics/**
if-no-files-found: ignore
retention-days: 7
full:
name: Full UI, sync, encryption, and Wayfern
if: ${{ inputs.smoke_only != true }}
if: ${{ inputs.smoke_only != true && inputs.suite != 'network' }}
runs-on: macos-latest
timeout-minutes: 40
@@ -125,13 +140,15 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: donutbrowser
persist-credentials: false
- name: Checkout private cross-platform WebDriver
- name: Checkout test driver
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY || 'zhom/tauri-cross-platform-webdriver' }}
repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY }}
token: ${{ secrets.TAURI_WEBDRIVER_TOKEN || github.token }}
path: tauri-cross-platform-webdriver
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
@@ -155,6 +172,7 @@ jobs:
with:
workspaces: |
donutbrowser/src-tauri -> target
donutbrowser/e2e/app -> target
tauri-cross-platform-webdriver -> target
- name: Install app dependencies
@@ -164,16 +182,88 @@ jobs:
- name: Run full isolated app suite
working-directory: donutbrowser
env:
DONUT_E2E_KEEP_ARTIFACTS: "1"
DONUT_E2E_SUITE: ${{ inputs.suite || 'full' }}
DONUT_E2E_SKIP_NETWORK_TEST: "1"
WAYFERN_TEST_TOKEN: ${{ secrets.WAYFERN_TEST_TOKEN }}
run: node e2e/run.mjs --suite=${DONUT_E2E_SUITE}
run: node e2e/run.mjs "--suite=${DONUT_E2E_SUITE}"
- name: Upload E2E diagnostics
if: always()
uses: actions/upload-artifact@v7
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: app-e2e-full-macos
path: ${{ runner.temp }}/donut-e2e-*
path: ${{ runner.temp }}/donut-e2e-*/diagnostics/**
if-no-files-found: ignore
retention-days: 7
network:
name: Real proxy and local WireGuard browser traffic
if: ${{ inputs.smoke_only != true && (inputs.suite == '' || inputs.suite == 'full' || inputs.suite == 'network') }}
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout Donut Browser
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: donutbrowser
persist-credentials: false
- name: Checkout test driver
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: ${{ vars.TAURI_WEBDRIVER_REPOSITORY }}
token: ${{ secrets.TAURI_WEBDRIVER_TOKEN || github.token }}
path: tauri-cross-platform-webdriver
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
run_install: false
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: donutbrowser/.node-version
cache: pnpm
cache-dependency-path: donutbrowser/pnpm-lock.yaml
- name: Install Rust
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # master
with:
toolchain: stable
- name: Cache Rust dependencies
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: |
donutbrowser/src-tauri -> target
donutbrowser/e2e/app -> target
tauri-cross-platform-webdriver -> target
- name: Install Tauri dependencies
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev xvfb openvpn
- name: Install app dependencies
working-directory: donutbrowser
run: pnpm install --frozen-lockfile
- name: Run isolated real-network suite
working-directory: donutbrowser
env:
WAYFERN_TEST_TOKEN: ${{ secrets.WAYFERN_TEST_TOKEN }}
RESIDENTIAL_PROXY_URL_ONE_SOCKS: ${{ secrets.RESIDENTIAL_PROXY_URL_ONE_SOCKS }}
RESIDENTIAL_PROXY_URL_ONE_HTTP: ${{ secrets.RESIDENTIAL_PROXY_URL_ONE_HTTP }}
run: xvfb-run -a pnpm e2e:network
- name: Upload E2E diagnostics
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: app-e2e-network-linux
path: ${{ runner.temp }}/donut-e2e-*/diagnostics/**
if-no-files-found: ignore
retention-days: 7
+8
View File
@@ -2,6 +2,12 @@ name: "CodeQL"
on:
workflow_call:
inputs:
checkout_ref:
description: Optional commit to check out instead of the triggering ref
required: false
type: string
default: ""
push:
branches: ["main"]
pull_request:
@@ -32,6 +38,8 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
ref: ${{ inputs.checkout_ref }}
- name: Set up pnpm package manager
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
+8 -4
View File
@@ -30,7 +30,8 @@ jobs:
name: Lint JavaScript/TypeScript
if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]'
uses: ./.github/workflows/lint-js.yml
secrets: inherit
with:
checkout_ref: ${{ github.event.pull_request.head.sha }}
permissions:
contents: read
@@ -38,7 +39,8 @@ jobs:
name: Lint Rust
if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]'
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
with:
checkout_ref: ${{ github.event.pull_request.head.sha }}
permissions:
contents: read
@@ -46,7 +48,8 @@ jobs:
name: CodeQL
if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]'
uses: ./.github/workflows/codeql.yml
secrets: inherit
with:
checkout_ref: ${{ github.event.pull_request.head.sha }}
permissions:
security-events: write
contents: read
@@ -57,7 +60,8 @@ jobs:
name: Spell Check
if: github.repository == 'zhom/donutbrowser' && github.actor == 'dependabot[bot]'
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
with:
checkout_ref: ${{ github.event.pull_request.head.sha }}
permissions:
contents: read
+16 -5
View File
@@ -11,6 +11,11 @@ on:
description: "Docker tag (e.g., v1.0.0)"
required: true
type: string
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
workflow_dispatch:
inputs:
tag:
@@ -43,23 +48,29 @@ jobs:
- name: Determine tags
id: tags
env:
INPUT_TAG: ${{ inputs.tag }}
EVENT_NAME: ${{ github.event_name }}
COMMIT_SHA: ${{ github.sha }}
run: |
TAGS=""
INPUT_TAG="${{ inputs.tag }}"
if [ -n "$INPUT_TAG" ]; then
# Called from release workflow or manual dispatch
if [[ ! "$INPUT_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
echo "Invalid Docker tag" >&2
exit 1
fi
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${INPUT_TAG}"
TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
elif [ "${{ github.event_name }}" = "push" ]; then
elif [ "$EVENT_NAME" = "push" ]; then
# Push to main (nightly): tag with nightly and commit SHA
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
SHORT_SHA=${COMMIT_SHA:0:7}
TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly"
TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${SHORT_SHA}"
fi
echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"
echo "Tags: ${TAGS}"
printf 'tags=%s\n' "$TAGS" >> "$GITHUB_OUTPUT"
- name: Build and push Docker image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a #v7.3.0
+3 -5
View File
@@ -29,8 +29,8 @@ jobs:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt
printf '%s' "$ISSUE_TITLE" | node scripts/redact-sensitive-text.mjs > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" | node scripts/redact-sensitive-text.mjs --issue-body > /tmp/issue-body.txt
- name: Build prompt
run: |
@@ -92,11 +92,9 @@ jobs:
sed -E 's/^```(json)?$//; s/```$//' /tmp/raw.txt > /tmp/result.json
if ! jq -e . /tmp/result.json >/dev/null 2>&1; then
echo "::warning::Model returned non-JSON; treating as compliant"
cat /tmp/raw.txt
echo '{"is_compliant": true, "non_compliance_reasons": []}' > /tmp/result.json
fi
echo "Result:"
cat /tmp/result.json
echo "Compliance response validated"
- name: Build comment
id: build
+47 -39
View File
@@ -14,7 +14,6 @@ permissions:
contents: read
issues: write
pull-requests: write
id-token: write
env:
# Single source of truth for the model used by both triage and composer.
@@ -49,8 +48,9 @@ jobs:
env:
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
node <<'EOF'
const fs = require('node:fs');
node --input-type=module <<'EOF'
import fs from 'node:fs';
import { redactIssueBody, redactSensitiveText } from './scripts/redact-sensitive-text.mjs';
const body = process.env.ISSUE_BODY || '';
// GitHub issue templates render fields as `### Heading\nValue` blocks.
// Split on `###` at line start to recover them.
@@ -61,29 +61,27 @@ jobs:
if (nl < 0) continue;
const heading = section.slice(0, nl).trim();
const value = section.slice(nl + 1).trim();
fields[heading] = value === '_No response_' ? '' : value;
const normalized = value === '_No response_' ? '' : value;
fields[heading] = heading === 'Error logs or screenshots'
? '[omitted from automated processing]'
: redactSensitiveText(normalized);
}
fs.writeFileSync('/tmp/issue-fields.json', JSON.stringify(fields, null, 2));
// Convenience extractions for the prompt — empty string if missing.
const get = (k) => fields[k] || '';
fs.writeFileSync('/tmp/issue-os.txt', get('Operating System'));
fs.writeFileSync('/tmp/issue-version.txt', get('Donut Browser version'));
fs.writeFileSync('/tmp/issue-wayfern-version.txt', get('Wayfern version'));
fs.writeFileSync('/tmp/issue-repro.txt', get('Steps to reproduce'));
fs.writeFileSync('/tmp/issue-logs.txt', get('Error logs or screenshots'));
fs.writeFileSync('/tmp/issue-what.txt', get('What happened?') || get('What do you want?'));
fs.writeFileSync('/tmp/issue-body.txt', redactIssueBody(body));
EOF
echo "Parsed fields:"
cat /tmp/issue-fields.json
- name: Build repo context
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
cp CLAUDE.md /tmp/repo-context.txt
printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt
printf '%s' "$ISSUE_TITLE" | node scripts/redact-sensitive-text.mjs > /tmp/issue-title.txt
# List all source files for the AI to choose from
find . -type f \( -name "*.rs" -o -name "*.ts" -o -name "*.tsx" \) \
@@ -199,7 +197,7 @@ jobs:
Return ONLY valid JSON. No preamble, no code fences. Schema:
{
"language": "en" or ISO 639-1 code,
"classification": one of ["bug-in-scope", "bug-template-violation", "feature-request", "fork-request", "regression", "ai-generated-junk", "question", "other"],
"classification": one of ["bug-in-scope", "bug-template-violation", "feature-request", "fork-request", "regression", "automated-content", "question", "other"],
"operating_system": "macos" | "windows" | "linux" | "unknown",
"is_paid_feature": true | false,
"user_followed_template": true | false,
@@ -211,11 +209,11 @@ jobs:
Classification guidance:
- "bug-template-violation": missing or filled-in nonsense for required template fields.
- "ai-generated-junk": cites fabricated "official docs" (context7, deepwiki, non-donutbrowser URLs) or has the polished AI-spam shape (long, structured, fabricated certainty).
- "automated-content": cites fabricated "official docs" (context7, deepwiki, non-donutbrowser URLs) or has a highly structured automated-submission pattern with fabricated certainty.
- "fork-request": asks for support of CloverLabsAI/VulpineOS/etc. forks.
- "regression": user names a prior version that worked.
File selection: pick files that an experienced reviewer would actually look at to act on this issue. If the issue is a fork-request or junk, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms.
File selection: pick files that an experienced reviewer would actually look at to act on this issue. For a fork request or automated-content classification, set files_to_read to []. Otherwise pick concrete files relevant to the symptoms.
TRIAGE_TAIL
} > /tmp/triage-system.txt
wc -c /tmp/triage-system.txt
@@ -238,7 +236,7 @@ jobs:
messages: [
{ role: "system", content: $system_prompt },
{ role: "user",
content: ("Issue title: " + $title + "\n\nBody:\n" + $body + "\n\nParsed template fields:\n" + $fields + "\n\nAll source files:\n" + $files) }
content: ("Issue title: " + $title + "\n\nSanitized body:\n" + $body + "\n\nSanitized template fields:\n" + $fields + "\n\nAll source files:\n" + $files) }
]
}')
@@ -249,14 +247,12 @@ jobs:
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/triage-raw.txt
# Strip ```json fences if the model couldn't help itself.
# Normalize optional markdown fences before parsing.
sed -E 's/^```(json)?$//; s/```$//' /tmp/triage-raw.txt > /tmp/triage.json
# Validate; if the model returned junk, fall back to a minimal stub so the
# composer still gets called and produces SOMETHING.
# Fall back to a safe classification when the response is not JSON.
if ! jq -e . /tmp/triage.json >/dev/null 2>&1; then
echo "::warning::Triage returned non-JSON; using fallback classification"
cat /tmp/triage-raw.txt
jq -n '{
language: "en",
classification: "bug-in-scope",
@@ -270,17 +266,16 @@ jobs:
}' > /tmp/triage.json
fi
echo "Triage result:"
cat /tmp/triage.json
echo "Triage response validated"
- name: Read files chosen by triage
run: |
: > /tmp/file-context.txt
# files_to_read may be empty (e.g. fork-request or junk) — that's fine.
# An empty file list is valid for classifications that need no source context.
jq -r '.files_to_read[]? // empty' /tmp/triage.json | while IFS= read -r filepath; do
filepath=$(echo "$filepath" | xargs)
[ -z "$filepath" ] && continue
# Reject paths that escape the repo or look fishy
# Reject paths that escape the repository.
case "$filepath" in
/*|*..*|*$'\n'*) continue ;;
esac
@@ -331,7 +326,7 @@ jobs:
The triage classification (`triage.classification`) determines the response shape:
- `bug-in-scope`: ask for what is missing using the user's reported OS log path. Be concrete about how to obtain logs.
- `bug-template-violation` or `ai-generated-junk`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Wayfern version, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com.
- `bug-template-violation` or `automated-content`: politely ask the user to refile using the bug-report template (the Operating System, Donut Browser version, Wayfern version, Steps to reproduce, Error logs sections). If they cited "documentation" from any non-`donutbrowser.com`/non-`github.com/zhom` URL (e.g. context7, deepwiki), gently note that those are AI-generated third-party summaries and the only authoritative sources are this repo and donutbrowser.com.
- `feature-request`: one neutral sentence acknowledging, then ask only what is genuinely needed (concrete use case, whether a workaround would suffice). Do NOT validate.
- `fork-request`: one neutral sentence acknowledging the request. Note that this would substantially increase support burden and the maintainer evaluates such requests on a case-by-case basis. Ask whether the alternative fork supports all platforms the user uses (macOS / Windows / Linux). No "clear enhancement" language.
- `regression`: do NOT call known/expected. Ask which exact previous version was the last working one, what changed in the user's environment between then and now, and the specific delta in symptoms.
@@ -408,8 +403,8 @@ jobs:
+ "Title: " + $title
+ "\nAuthor: " + $author
+ "\n\n## Triage result\n" + $triage
+ "\n\n## Parsed template fields\n" + $fields
+ "\n\n## Raw issue body\n" + $body
+ "\n\n## Sanitized template fields\n" + $fields
+ "\n\n## Sanitized issue body\n" + $body
+ "\n\n## Source files (selected by triage)\n" + $files) }
]
}')
@@ -423,8 +418,6 @@ jobs:
if [ ! -s /tmp/ai-comment.txt ]; then
echo "::error::Composer returned empty response"
echo "Raw response:"
echo "$RESPONSE"
exit 1
fi
@@ -484,8 +477,9 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}
run: |
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \
gh api --paginate "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100" \
--jq '.[] | "- \(.filename) (\(.status)) +\(.additions)/-\(.deletions)"' \
> /tmp/pr-files.txt
@@ -499,14 +493,27 @@ jobs:
cp CLAUDE.md /tmp/repo-context.txt
: > /tmp/related-file-contents.txt
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' | while IFS= read -r filepath; do
if [ -f "$filepath" ] && file --mime "$filepath" | grep -q "text/"; then
echo "=== $filepath (full file) ===" >> /tmp/related-file-contents.txt
cat "$filepath" >> /tmp/related-file-contents.txt
echo "" >> /tmp/related-file-contents.txt
gh api --paginate "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files?per_page=100" \
--jq '.[] | select(.status != "removed") | [.filename, .sha] | @tsv' |
while IFS=$'\t' read -r filepath blob_sha; do
case "$filepath" in
/*|*..*|*$'\n'*) continue ;;
esac
blob_file=$(mktemp)
if gh api "/repos/$HEAD_REPOSITORY/git/blobs/$blob_sha" --jq .content \
| tr -d '\n' | base64 --decode > "$blob_file" 2>/dev/null \
&& file --mime "$blob_file" | grep -q "text/"; then
echo "=== $filepath (head revision) ===" >> /tmp/related-file-contents.txt
cat "$blob_file" >> /tmp/related-file-contents.txt
echo "" >> /tmp/related-file-contents.txt
fi
rm -f "$blob_file"
done
head -c 100000 /tmp/related-file-contents.txt > /tmp/pr-file-context.txt
node scripts/redact-sensitive-text.mjs < /tmp/pr-diff.txt > /tmp/pr-diff.safe.txt
mv /tmp/pr-diff.safe.txt /tmp/pr-diff.txt
node scripts/redact-sensitive-text.mjs < /tmp/pr-file-context.txt > /tmp/pr-file-context.safe.txt
mv /tmp/pr-file-context.safe.txt /tmp/pr-file-context.txt
- name: Analyze PR with AI
env:
@@ -525,8 +532,8 @@ jobs:
GREETING='This is a first-time contributor. Start your comment with: "Thanks for your first PR!"'
fi
printf '%s' "$PR_TITLE" > /tmp/pr-title.txt
printf '%s' "${PR_BODY:-}" > /tmp/pr-body.txt
printf '%s' "$PR_TITLE" | node scripts/redact-sensitive-text.mjs > /tmp/pr-title.txt
printf '%s' "${PR_BODY:-}" | node scripts/redact-sensitive-text.mjs > /tmp/pr-body.txt
printf '%s' "$PR_AUTHOR" > /tmp/pr-author.txt
printf '%s' "$PR_BASE" > /tmp/pr-base.txt
printf '%s' "$PR_HEAD" > /tmp/pr-head.txt
@@ -550,7 +557,7 @@ jobs:
messages: [
{
role: "system",
content: ("You are a code review bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nContributing guidelines:\n" + $contributing + "\n\nYou have access to the full changed files and the diff. Use them to give a substantive review.\n\nReview this PR and produce a single comment. Format:\n\n1. One sentence summarizing what this PR does and whether the approach is sound.\n2. **Code review** - Specific observations about the actual code changes. Mention file names and what you see in the diff. Look for:\n - Bugs or logic errors in the changed code\n - Security issues (SQL injection, path traversal, XSS, command injection)\n - Missing error handling or edge cases\n - Breaking changes to existing APIs or behavior\n - If UI text was added/changed, check if all 7 translation files (en, es, fr, ja, pt, ru, zh) in src/i18n/locales/ were updated\n - If Tauri commands were added/removed, the unused-commands test in lib.rs needs updating\n3. **Suggestions** - Concrete improvements if any. Skip if the PR looks good.\n\nRules:\n- Be substantive. Review the actual diff, not just the description.\n- Do NOT nitpick formatting or style — the project has automated linting (biome + clippy + rustfmt).\n- Do NOT just summarize the PR description back to the user — they wrote it, they know what it says.\n- If the PR is good, say so briefly.\n- Never exceed 20 lines.")
content: ("You are a code review bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nContributing guidelines:\n" + $contributing + "\n\nYou have access to sanitized head-revision contents for changed files and a sanitized diff. Use them to give a substantive review.\n\nReview this PR and produce a single comment. Format:\n\n1. One sentence summarizing what this PR does and whether the approach is sound.\n2. **Code review** - Specific observations about the actual code changes. Mention file names and what you see in the diff. Look for:\n - Bugs or logic errors in the changed code\n - Security issues (SQL injection, path traversal, XSS, command injection)\n - Missing error handling or edge cases\n - Breaking changes to existing APIs or behavior\n - If UI text was added or changed, verify the key exists in every JSON file under src/i18n/locales/\n - If Tauri commands were added or removed, verify e2e/coverage-map.mjs is updated exactly once per command\n3. **Suggestions** - Concrete improvements if any. Skip if the PR looks good.\n\nRules:\n- Be substantive. Review the actual diff, not just the description.\n- Do NOT nitpick formatting or style — the project has automated linting (biome + clippy + rustfmt).\n- Do NOT just summarize the PR description back to the user — they wrote it, they know what it says.\n- If the PR is good, say so briefly.\n- Never exceed 20 lines.")
},
{
role: "user",
@@ -577,8 +584,6 @@ jobs:
if [ ! -s /tmp/ai-comment.txt ]; then
echo "::error::AI response was empty"
echo "Raw response:"
echo "$RESPONSE"
exit 1
fi
@@ -593,6 +598,9 @@ jobs:
if: |
github.repository == 'zhom/donutbrowser' &&
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR') &&
(contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
+8
View File
@@ -4,6 +4,12 @@ name: Lint Node.js
on:
workflow_call:
inputs:
checkout_ref:
description: Optional commit to check out instead of the triggering ref
required: false
type: string
default: ""
push:
branches:
- main
@@ -35,6 +41,8 @@ jobs:
- name: Checkout repository code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
ref: ${{ inputs.checkout_ref }}
- name: Set up pnpm package manager
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
+8
View File
@@ -4,6 +4,12 @@ name: Lint Rust
on:
workflow_call:
inputs:
checkout_ref:
description: Optional commit to check out instead of the triggering ref
required: false
type: string
default: ""
push:
branches:
- main
@@ -42,6 +48,8 @@ jobs:
- name: Checkout repository code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
ref: ${{ inputs.checkout_ref }}
- name: Set up pnpm package manager
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
+3 -4
View File
@@ -15,14 +15,12 @@ jobs:
lint-js:
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
lint-rust:
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
@@ -41,14 +39,15 @@ jobs:
sync-e2e:
name: Sync E2E Tests
uses: ./.github/workflows/sync-e2e.yml
secrets: inherit
permissions:
contents: read
app-e2e:
name: Cross-platform App E2E
uses: ./.github/workflows/app-e2e.yml
secrets: inherit
secrets:
TAURI_WEBDRIVER_TOKEN: ${{ secrets.TAURI_WEBDRIVER_TOKEN }}
WAYFERN_TEST_TOKEN: ${{ secrets.WAYFERN_TEST_TOKEN }}
permissions:
contents: read
+15 -15
View File
@@ -31,17 +31,24 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_TAG: ${{ inputs.tag }}
EVENT_NAME: ${{ github.event_name }}
WORKFLOW_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
REPOSITORY: ${{ github.repository }}
run: |
if [[ -n "${INPUT_TAG:-}" ]]; then
echo "tag=${INPUT_TAG}" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event_name }}" == "workflow_run" ]]; then
TAG="$INPUT_TAG"
elif [[ "$EVENT_NAME" == "workflow_run" ]]; then
# The Release workflow is triggered by a tag push (v*),
# so head_branch is the tag name
echo "tag=${{ github.event.workflow_run.head_branch }}" >> "$GITHUB_OUTPUT"
TAG="$WORKFLOW_HEAD_BRANCH"
else
TAG=$(gh release view --repo "${{ github.repository }}" --json tagName -q .tagName)
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
TAG=$(gh release view --repo "$REPOSITORY" --json tagName -q .tagName)
fi
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid release tag" >&2
exit 1
fi
printf 'tag=%s\n' "$TAG" >> "$GITHUB_OUTPUT"
- name: Install tools
run: |
@@ -59,19 +66,12 @@ jobs:
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ steps.tag.outputs.tag }}
run: |
# GitHub injects secrets verbatim. If a value was pasted with
# surrounding quotes or a trailing newline — the local .env wraps all
# four R2_* values in double quotes — it reaches the script malformed:
# e.g. an endpoint of https://"host" yields
# `Could not connect to the endpoint URL`, and a quoted key yields
# `Unauthorized`. The local run is unaffected because publish-repo.sh
# sources .env through bash, which strips the quotes; CI has no .env,
# so strip here. No-op when the secrets are already clean. The script
# itself is intentionally left untouched.
# Normalize accidental quotes and whitespace in configured secrets.
strip() { printf '%s' "$1" | tr -d '\r\n' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"\(.*\)"$/\1/' -e "s/^'\(.*\)'\$/\1/"; }
export R2_ACCESS_KEY_ID="$(strip "$R2_ACCESS_KEY_ID")"
export R2_SECRET_ACCESS_KEY="$(strip "$R2_SECRET_ACCESS_KEY")"
export R2_ENDPOINT_URL="$(strip "$R2_ENDPOINT_URL")"
export R2_BUCKET_NAME="$(strip "$R2_BUCKET_NAME")"
bash scripts/publish-repo.sh "${{ steps.tag.outputs.tag }}"
bash scripts/publish-repo.sh "$RELEASE_TAG"
+7 -6
View File
@@ -37,7 +37,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
@@ -45,7 +44,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
@@ -53,7 +51,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: CodeQL
uses: ./.github/workflows/codeql.yml
secrets: inherit
permissions:
security-events: write
contents: read
@@ -64,7 +61,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
@@ -218,6 +214,7 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TARGET: ${{ matrix.target }}
# tauri-action invokes `pnpm tauri build`, which runs
# `beforeBuildCommand` from tauri.conf.json. That rebuilds the
# frontend in its own subprocess, so the env var MUST be forwarded
@@ -559,7 +556,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger Cloudflare Pages deployment
run: curl -fsSL -X POST "${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}"
env:
DEPLOYMENT_HOOK: ${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}
run: curl -fsSL -X POST "$DEPLOYMENT_HOOK"
docker:
if: github.repository == 'zhom/donutbrowser'
@@ -567,7 +566,9 @@ jobs:
uses: ./.github/workflows/docker-sync.yml
with:
tag: ${{ github.ref_name }}
secrets: inherit
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
update-flake:
if: github.repository == 'zhom/donutbrowser'
+4 -5
View File
@@ -44,7 +44,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: Lint JavaScript/TypeScript
uses: ./.github/workflows/lint-js.yml
secrets: inherit
permissions:
contents: read
@@ -52,7 +51,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: Lint Rust
uses: ./.github/workflows/lint-rs.yml
secrets: inherit
permissions:
contents: read
@@ -60,7 +58,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: CodeQL
uses: ./.github/workflows/codeql.yml
secrets: inherit
permissions:
security-events: write
contents: read
@@ -71,7 +68,6 @@ jobs:
if: github.repository == 'zhom/donutbrowser'
name: Spell Check
uses: ./.github/workflows/spellcheck.yml
secrets: inherit
permissions:
contents: read
@@ -241,6 +237,7 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
TARGET: ${{ matrix.target }}
# tauri-action's inner `pnpm tauri build` re-runs beforeBuildCommand
# which rebuilds dist/ in a subprocess. The env var must be here too.
NEXT_PUBLIC_TURNSTILE: ${{ secrets.NEXT_PUBLIC_TURNSTILE }}
@@ -419,7 +416,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Trigger Cloudflare Pages deployment
run: curl -fsSL -X POST "${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}"
env:
DEPLOYMENT_HOOK: ${{ secrets.CLOUDFLARE_WEB_DEPLOYMENT_HOOK }}
run: curl -fsSL -X POST "$DEPLOYMENT_HOOK"
notify-discord:
if: github.repository == 'zhom/donutbrowser'
+8
View File
@@ -5,6 +5,12 @@ permissions:
on:
workflow_call:
inputs:
checkout_ref:
description: Optional commit to check out instead of the triggering ref
required: false
type: string
default: ""
push:
branches: ["main"]
pull_request:
@@ -22,5 +28,7 @@ jobs:
steps:
- name: Checkout Actions Repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
ref: ${{ inputs.checkout_ref }}
- name: Spell Check Repo
uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 #v1.48.0
+4 -4
View File
@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 #v6.0.9
@@ -40,7 +40,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
cache: "pnpm"
@@ -73,7 +73,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v7.0.0
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Start MinIO
run: |
@@ -99,7 +99,7 @@ jobs:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
cache: "pnpm"