mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 17:57:50 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e006d56387 | |||
| 43f9f02029 | |||
| 839265de35 | |||
| 0d85b61c96 | |||
| f581b6ec59 | |||
| 43c86c2dfb | |||
| ddfdf68dd1 |
@@ -1,108 +0,0 @@
|
|||||||
name: Compliance Close
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Every 30 minutes; the actual close decision uses comment age, so the cron
|
|
||||||
# cadence only bounds how stale the closure can get past the 24-hour mark.
|
|
||||||
- cron: "*/30 * * * *"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close-non-compliant:
|
|
||||||
if: github.repository == 'zhom/donutbrowser'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Close non-compliant issues and PRs after 24 hours
|
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { data: items } = await github.rest.issues.listForRepo({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
labels: 'needs:compliance',
|
|
||||||
state: 'open',
|
|
||||||
per_page: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
core.info('No open issues/PRs with needs:compliance label');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const window_ms = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const isPR = !!item.pull_request;
|
|
||||||
const kind = isPR ? 'PR' : 'issue';
|
|
||||||
|
|
||||||
const { data: comments } = await github.rest.issues.listComments({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use the OLDEST compliance sentinel as the start of the 24-hour
|
|
||||||
// window so back-and-forth edits don't reset the clock.
|
|
||||||
const sentinel = comments
|
|
||||||
.filter(c => c.body && c.body.includes('<!-- issue-compliance -->'))
|
|
||||||
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at))[0];
|
|
||||||
|
|
||||||
if (!sentinel) {
|
|
||||||
core.info(`${kind} #${item.number} has needs:compliance label but no compliance comment; skipping`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const age_ms = now - new Date(sentinel.created_at).getTime();
|
|
||||||
if (age_ms < window_ms) {
|
|
||||||
const hours = (age_ms / (60 * 60 * 1000)).toFixed(1);
|
|
||||||
core.info(`${kind} #${item.number} still within 24-hour window (${hours}h elapsed)`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMessage = isPR
|
|
||||||
? 'This pull request has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/main/CONTRIBUTING.md) within the 24-hour window.\n\nFeel free to open a new pull request that follows our guidelines.'
|
|
||||||
: 'This issue has been automatically closed because it was not updated to meet our [contributing guidelines](../blob/main/CONTRIBUTING.md) within the 24-hour window.\n\nFeel free to open a new issue that follows our issue templates.';
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
body: closeMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await github.rest.issues.removeLabel({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
name: 'needs:compliance',
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
core.info(`Could not remove needs:compliance label from #${item.number}: ${e.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPR) {
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
pull_number: item.number,
|
|
||||||
state: 'closed',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await github.rest.issues.update({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
issue_number: item.number,
|
|
||||||
state: 'closed',
|
|
||||||
state_reason: 'not_planned',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
core.info(`Closed non-compliant ${kind} #${item.number} after 24-hour window`);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ name: Issue Compliance Check
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, edited]
|
types: [opened]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -13,7 +13,12 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-compliance:
|
check-compliance:
|
||||||
if: github.repository == 'zhom/donutbrowser' && github.event.action == 'opened'
|
# Maintainers' own issues are exempt — they open quick tracking issues
|
||||||
|
# without the template on purpose. Everyone else is checked.
|
||||||
|
if: >-
|
||||||
|
github.repository == 'zhom/donutbrowser' &&
|
||||||
|
github.event.issue.author_association != 'OWNER' &&
|
||||||
|
github.event.issue.author_association != 'MEMBER'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -44,7 +49,7 @@ jobs:
|
|||||||
- A feature request gives no use case at all
|
- A feature request gives no use case at all
|
||||||
- The author left required fields empty (Operating System, Donut Browser version, Which browser is affected, Steps to reproduce on bug reports)
|
- The author left required fields empty (Operating System, Donut Browser version, Which browser is affected, Steps to reproduce on bug reports)
|
||||||
|
|
||||||
Do NOT flag for missing optional fields, missing screenshots, short titles, or stylistic issues. Be conservative.
|
Do NOT flag for missing optional fields, missing screenshots, short titles, or stylistic issues. Be conservative — a non-compliant verdict closes the issue, so only flag a genuine template violation.
|
||||||
|
|
||||||
## Output schema
|
## Output schema
|
||||||
{
|
{
|
||||||
@@ -83,7 +88,7 @@ jobs:
|
|||||||
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/raw.txt
|
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/raw.txt
|
||||||
|
|
||||||
# Strip accidental markdown fences and parse. On parse failure, fall back
|
# Strip accidental markdown fences and parse. On parse failure, fall back
|
||||||
# to a noop result so the workflow doesn't fail the issue author's run.
|
# to a compliant result so a flaky model never closes a legitimate issue.
|
||||||
sed -E 's/^```(json)?$//; s/```$//' /tmp/raw.txt > /tmp/result.json
|
sed -E 's/^```(json)?$//; s/```$//' /tmp/raw.txt > /tmp/result.json
|
||||||
if ! jq -e . /tmp/result.json >/dev/null 2>&1; then
|
if ! jq -e . /tmp/result.json >/dev/null 2>&1; then
|
||||||
echo "::warning::Model returned non-JSON; treating as compliant"
|
echo "::warning::Model returned non-JSON; treating as compliant"
|
||||||
@@ -94,6 +99,7 @@ jobs:
|
|||||||
cat /tmp/result.json
|
cat /tmp/result.json
|
||||||
|
|
||||||
- name: Build comment
|
- name: Build comment
|
||||||
|
id: build
|
||||||
run: |
|
run: |
|
||||||
python3 - <<'EOF'
|
python3 - <<'EOF'
|
||||||
import json, os
|
import json, os
|
||||||
@@ -103,167 +109,25 @@ jobs:
|
|||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
if not compliant:
|
if not compliant:
|
||||||
parts.append('<!-- issue-compliance -->')
|
parts.append("This issue was automatically closed because it doesn't follow our [issue templates](../issues/new/choose).")
|
||||||
parts.append("This issue doesn't fully meet our [contributing guidelines](../blob/main/CONTRIBUTING.md).")
|
|
||||||
parts.append('')
|
parts.append('')
|
||||||
parts.append('**What needs to be fixed:**')
|
parts.append('**What was missing:**')
|
||||||
for reason in reasons:
|
for reason in reasons:
|
||||||
parts.append(f'- {reason}')
|
parts.append(f'- {reason}')
|
||||||
parts.append('')
|
parts.append('')
|
||||||
parts.append('Please edit this issue to address the above within **24 hours**, or it will be automatically closed.')
|
parts.append('If this is a real bug or feature request, please open a new issue using the **Bug Report** or **Feature Request** template and fill in the required fields. Issues that ignore the template are not triaged.')
|
||||||
parts.append('')
|
|
||||||
parts.append('If you believe this was flagged incorrectly, please let a maintainer know.')
|
|
||||||
|
|
||||||
comment = '\n'.join(parts).strip()
|
comment = '\n'.join(parts).strip()
|
||||||
open('/tmp/comment.md', 'w').write(comment)
|
open('/tmp/comment.md', 'w').write(comment)
|
||||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
|
with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
|
||||||
fh.write(f'has_comment={"true" if comment else "false"}\n')
|
|
||||||
fh.write(f'non_compliant={"true" if not compliant else "false"}\n')
|
fh.write(f'non_compliant={"true" if not compliant else "false"}\n')
|
||||||
EOF
|
EOF
|
||||||
id: build
|
|
||||||
|
|
||||||
- name: Post comment
|
- name: Comment and close non-compliant issue
|
||||||
if: steps.build.outputs.has_comment == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
run: |
|
|
||||||
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file /tmp/comment.md
|
|
||||||
|
|
||||||
- name: Apply needs:compliance label
|
|
||||||
if: steps.build.outputs.non_compliant == 'true'
|
if: steps.build.outputs.non_compliant == 'true'
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
run: |
|
|
||||||
gh issue edit "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "needs:compliance"
|
|
||||||
|
|
||||||
recheck-compliance:
|
|
||||||
# When a flagged issue is edited, re-check. If now compliant: remove label,
|
|
||||||
# delete the previous compliance comment, and thank the author. If still
|
|
||||||
# non-compliant: leave label and post an updated note.
|
|
||||||
if: >
|
|
||||||
github.repository == 'zhom/donutbrowser' &&
|
|
||||||
github.event.action == 'edited' &&
|
|
||||||
contains(github.event.issue.labels.*.name, 'needs:compliance')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
|
|
||||||
- name: Gather context
|
|
||||||
env:
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Build prompt
|
|
||||||
run: |
|
|
||||||
cat > /tmp/system.txt <<'PROMPT'
|
|
||||||
You are re-checking a GitHub issue that was previously flagged as not meeting template requirements. Return ONLY a single JSON object, no prose, no markdown fences.
|
|
||||||
|
|
||||||
Project: Donut Browser. There are three valid templates:
|
|
||||||
- Bug Report (Description + Operating System + Donut Browser version + Which browser is affected + Steps to reproduce + Error logs/screenshots fields)
|
|
||||||
- Feature Request (description + verification checkbox)
|
|
||||||
- Question (free form)
|
|
||||||
|
|
||||||
## Flag NON-compliant ONLY when at least one of these is true
|
|
||||||
- The issue body is empty or contains only placeholder text from the template
|
|
||||||
- The issue is an obvious AI-generated wall of text with no real specifics
|
|
||||||
- A bug report has no reproduction information or no error description
|
|
||||||
- A feature request gives no use case at all
|
|
||||||
- The author left required fields empty (Operating System, Donut Browser version, Which browser is affected, Steps to reproduce on bug reports)
|
|
||||||
|
|
||||||
Do NOT flag for missing optional fields, missing screenshots, short titles, or stylistic issues. Be conservative.
|
|
||||||
|
|
||||||
## Output schema
|
|
||||||
{
|
|
||||||
"is_compliant": true | false,
|
|
||||||
"non_compliance_reasons": ["short bullet", ...]
|
|
||||||
}
|
|
||||||
PROMPT
|
|
||||||
|
|
||||||
- name: Call OpenRouter
|
|
||||||
env:
|
|
||||||
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
||||||
run: |
|
|
||||||
PAYLOAD=$(jq -n \
|
|
||||||
--arg model "$MODEL" \
|
|
||||||
--rawfile system_prompt /tmp/system.txt \
|
|
||||||
--rawfile title /tmp/issue-title.txt \
|
|
||||||
--rawfile body /tmp/issue-body.txt \
|
|
||||||
'{
|
|
||||||
model: $model,
|
|
||||||
messages: [
|
|
||||||
{ role: "system", content: $system_prompt },
|
|
||||||
{ role: "user", content: ("Title: " + $title + "\n\nBody:\n" + $body) }
|
|
||||||
],
|
|
||||||
response_format: { type: "json_object" }
|
|
||||||
}')
|
|
||||||
|
|
||||||
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
|
|
||||||
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$PAYLOAD")
|
|
||||||
|
|
||||||
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/raw.txt
|
|
||||||
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; assuming still non-compliant"
|
|
||||||
echo '{"is_compliant": false, "non_compliance_reasons": ["unable to parse model output"]}' > /tmp/result.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Resolve compliance state
|
|
||||||
id: resolve
|
|
||||||
run: |
|
|
||||||
IS_COMPLIANT=$(jq -r '.is_compliant // false' /tmp/result.json)
|
|
||||||
echo "is_compliant=$IS_COMPLIANT" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Clear compliance label and acknowledge fix
|
|
||||||
if: steps.resolve.outputs.is_compliant == 'true'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
run: |
|
|
||||||
gh issue edit "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --remove-label "needs:compliance" || true
|
|
||||||
|
|
||||||
# Delete the previous <!-- issue-compliance --> sentinel comment so
|
|
||||||
# the thread is clean once the author has addressed the issue.
|
|
||||||
COMMENT_ID=$(gh api "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \
|
|
||||||
--jq '[.[] | select(.body | contains("<!-- issue-compliance -->"))][-1].id // empty')
|
|
||||||
if [ -n "$COMMENT_ID" ]; then
|
|
||||||
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
|
|
||||||
--body "Thanks for updating the issue."
|
|
||||||
|
|
||||||
- name: Build follow-up comment
|
|
||||||
if: steps.resolve.outputs.is_compliant != 'true'
|
|
||||||
run: |
|
|
||||||
python3 - <<'EOF'
|
|
||||||
import json
|
|
||||||
r = json.load(open('/tmp/result.json'))
|
|
||||||
reasons = r.get('non_compliance_reasons') or []
|
|
||||||
parts = [
|
|
||||||
'<!-- issue-compliance -->',
|
|
||||||
'This issue still does not meet our [contributing guidelines](../blob/main/CONTRIBUTING.md).',
|
|
||||||
'',
|
|
||||||
'**What still needs to be fixed:**',
|
|
||||||
]
|
|
||||||
for reason in reasons:
|
|
||||||
parts.append(f'- {reason}')
|
|
||||||
parts.append('')
|
|
||||||
parts.append('Please edit this issue to address the above within **24 hours**, or it will be automatically closed.')
|
|
||||||
open('/tmp/comment.md', 'w').write('\n'.join(parts))
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Post follow-up comment
|
|
||||||
if: steps.resolve.outputs.is_compliant != 'true'
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
run: |
|
run: |
|
||||||
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file /tmp/comment.md
|
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file /tmp/comment.md
|
||||||
|
gh issue close "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --reason "not planned"
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ jobs:
|
|||||||
github.event.workflow_run.conclusion == 'success')
|
github.event.workflow_run.conclusion == 'success')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||||
|
|
||||||
- name: Determine release tag
|
- name: Determine release tag
|
||||||
id: tag
|
id: tag
|
||||||
env:
|
env:
|
||||||
@@ -40,182 +43,32 @@ jobs:
|
|||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Configure aws-cli for R2
|
- name: Install tools (dpkg-dev, createrepo-c, aws-cli v1)
|
||||||
# aws-cli v2.23+ sends integrity checksums by default; Cloudflare R2
|
|
||||||
# rejects those headers with `Unauthorized` on ListObjectsV2.
|
|
||||||
# Also normalise the endpoint URL (must start with https://).
|
|
||||||
# Both values propagate to later steps via $GITHUB_ENV.
|
|
||||||
env:
|
|
||||||
RAW_ENDPOINT: ${{ secrets.R2_ENDPOINT_URL }}
|
|
||||||
run: |
|
|
||||||
endpoint="$RAW_ENDPOINT"
|
|
||||||
if [[ "$endpoint" != https://* && "$endpoint" != http://* ]]; then
|
|
||||||
endpoint="https://$endpoint"
|
|
||||||
fi
|
|
||||||
echo "R2_ENDPOINT=$endpoint" >> "$GITHUB_ENV"
|
|
||||||
echo "AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED" >> "$GITHUB_ENV"
|
|
||||||
echo "AWS_RESPONSE_CHECKSUM_VALIDATION=WHEN_REQUIRED" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Install tools
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y dpkg-dev createrepo-c python3-pip
|
sudo apt-get install -y dpkg-dev createrepo-c python3-pip
|
||||||
# Remove pre-installed aws-cli v2 — it sends CRC64NVME checksums
|
# GitHub runners ship aws-cli v2, which sends CRC64NVME integrity
|
||||||
# that Cloudflare R2 rejects with Unauthorized, and the s3transfer
|
# checksums that Cloudflare R2 rejects with `Unauthorized` on
|
||||||
# lib has a confirmed bug where WHEN_REQUIRED is silently ignored
|
# ListObjectsV2 (the call behind `aws s3 sync`). aws-cli v1 predates
|
||||||
# (boto/s3transfer#327). Install aws-cli v1 via pip instead.
|
# that behavior — the same reason scripts/publish-repo.sh works in
|
||||||
|
# Docker. Remove v2 and install v1 system-wide so it's the binary on
|
||||||
|
# PATH within this job, and fail fast if v2 somehow survives rather
|
||||||
|
# than letting the publish step die on an opaque Unauthorized later.
|
||||||
sudo rm -f /usr/local/bin/aws /usr/local/bin/aws_completer
|
sudo rm -f /usr/local/bin/aws /usr/local/bin/aws_completer
|
||||||
sudo rm -rf /usr/local/aws-cli
|
sudo rm -rf /usr/local/aws-cli
|
||||||
pip3 install --break-system-packages awscli
|
sudo pip3 install --break-system-packages 'awscli<2'
|
||||||
# Ensure pip-installed aws is on PATH (pip may install to ~/.local/bin)
|
hash -r
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
|
||||||
aws --version
|
aws --version
|
||||||
|
case "$(aws --version 2>&1)" in
|
||||||
|
aws-cli/1.*) ;;
|
||||||
|
*) echo "::error::Expected aws-cli v1 but got: $(aws --version 2>&1)"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
- name: Download packages from GitHub release
|
- name: Publish DEB & RPM repositories to R2
|
||||||
env:
|
env:
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
|
||||||
|
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG: ${{ steps.tag.outputs.tag }}
|
run: bash scripts/publish-repo.sh "${{ steps.tag.outputs.tag }}"
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/packages
|
|
||||||
gh release download "$TAG" \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--pattern "*.deb" \
|
|
||||||
--dir /tmp/packages
|
|
||||||
gh release download "$TAG" \
|
|
||||||
--repo "${{ github.repository }}" \
|
|
||||||
--pattern "*.rpm" \
|
|
||||||
--dir /tmp/packages
|
|
||||||
echo "Downloaded packages:"
|
|
||||||
ls -lh /tmp/packages/
|
|
||||||
|
|
||||||
- name: Build DEB repository
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET_NAME }}
|
|
||||||
run: |
|
|
||||||
DEB_DIR="/tmp/repo/deb"
|
|
||||||
mkdir -p "$DEB_DIR/pool/main"
|
|
||||||
mkdir -p "$DEB_DIR/dists/stable/main/binary-amd64"
|
|
||||||
mkdir -p "$DEB_DIR/dists/stable/main/binary-arm64"
|
|
||||||
|
|
||||||
# Sync existing pool from R2 (incremental)
|
|
||||||
aws s3 sync "s3://${R2_BUCKET}/deb/pool" "$DEB_DIR/pool" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Copy new .deb files into pool
|
|
||||||
cp /tmp/packages/*.deb "$DEB_DIR/pool/main/" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Generate Packages and Packages.gz for each arch
|
|
||||||
for arch in amd64 arm64; do
|
|
||||||
BINARY_DIR="$DEB_DIR/dists/stable/main/binary-${arch}"
|
|
||||||
(cd "$DEB_DIR" && dpkg-scanpackages --arch "$arch" pool/main) \
|
|
||||||
> "$BINARY_DIR/Packages"
|
|
||||||
gzip -9c "$BINARY_DIR/Packages" > "$BINARY_DIR/Packages.gz"
|
|
||||||
echo " $arch: $(grep -c '^Package:' "$BINARY_DIR/Packages" 2>/dev/null || echo 0) package(s)"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Generate Release file
|
|
||||||
{
|
|
||||||
echo "Origin: Donut Browser"
|
|
||||||
echo "Label: Donut Browser"
|
|
||||||
echo "Suite: stable"
|
|
||||||
echo "Codename: stable"
|
|
||||||
echo "Architectures: amd64 arm64"
|
|
||||||
echo "Components: main"
|
|
||||||
echo "Date: $(date -u '+%a, %d %b %Y %H:%M:%S UTC')"
|
|
||||||
echo "MD5Sum:"
|
|
||||||
for arch in amd64 arm64; do
|
|
||||||
for file in "main/binary-${arch}/Packages" "main/binary-${arch}/Packages.gz"; do
|
|
||||||
filepath="$DEB_DIR/dists/stable/$file"
|
|
||||||
if [[ -f "$filepath" ]]; then
|
|
||||||
size=$(wc -c < "$filepath")
|
|
||||||
md5=$(md5sum "$filepath" | awk '{print $1}')
|
|
||||||
printf " %s %8d %s\n" "$md5" "$size" "$file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
echo "SHA256:"
|
|
||||||
for arch in amd64 arm64; do
|
|
||||||
for file in "main/binary-${arch}/Packages" "main/binary-${arch}/Packages.gz"; do
|
|
||||||
filepath="$DEB_DIR/dists/stable/$file"
|
|
||||||
if [[ -f "$filepath" ]]; then
|
|
||||||
size=$(wc -c < "$filepath")
|
|
||||||
sha256=$(sha256sum "$filepath" | awk '{print $1}')
|
|
||||||
printf " %s %8d %s\n" "$sha256" "$size" "$file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
|
||||||
} > "$DEB_DIR/dists/stable/Release"
|
|
||||||
|
|
||||||
echo "DEB Release file created."
|
|
||||||
|
|
||||||
- name: Build RPM repository
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET_NAME }}
|
|
||||||
run: |
|
|
||||||
RPM_DIR="/tmp/repo/rpm"
|
|
||||||
mkdir -p "$RPM_DIR/x86_64"
|
|
||||||
mkdir -p "$RPM_DIR/aarch64"
|
|
||||||
|
|
||||||
# Sync existing RPMs from R2 (incremental)
|
|
||||||
aws s3 sync "s3://${R2_BUCKET}/rpm/x86_64" "$RPM_DIR/x86_64" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" --exclude "repodata/*" 2>/dev/null || true
|
|
||||||
aws s3 sync "s3://${R2_BUCKET}/rpm/aarch64" "$RPM_DIR/aarch64" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" --exclude "repodata/*" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Copy new .rpm files into arch directories
|
|
||||||
for rpm in /tmp/packages/*.rpm; do
|
|
||||||
[[ -f "$rpm" ]] || continue
|
|
||||||
filename=$(basename "$rpm")
|
|
||||||
if [[ "$filename" == *x86_64* ]]; then
|
|
||||||
cp "$rpm" "$RPM_DIR/x86_64/"
|
|
||||||
elif [[ "$filename" == *aarch64* ]]; then
|
|
||||||
cp "$rpm" "$RPM_DIR/aarch64/"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Generate repodata
|
|
||||||
createrepo_c --update "$RPM_DIR"
|
|
||||||
echo "RPM repodata created."
|
|
||||||
|
|
||||||
- name: Upload to R2
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET_NAME }}
|
|
||||||
run: |
|
|
||||||
echo "Uploading DEB repository..."
|
|
||||||
aws s3 sync /tmp/repo/deb/dists "s3://${R2_BUCKET}/deb/dists" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" --delete
|
|
||||||
aws s3 sync /tmp/repo/deb/pool "s3://${R2_BUCKET}/deb/pool" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT"
|
|
||||||
|
|
||||||
echo "Uploading RPM repository..."
|
|
||||||
aws s3 sync /tmp/repo/rpm "s3://${R2_BUCKET}/rpm" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT"
|
|
||||||
|
|
||||||
- name: Verify upload
|
|
||||||
env:
|
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
||||||
AWS_DEFAULT_REGION: auto
|
|
||||||
R2_BUCKET: ${{ secrets.R2_BUCKET_NAME }}
|
|
||||||
TAG: ${{ steps.tag.outputs.tag }}
|
|
||||||
run: |
|
|
||||||
echo "Published repos for $TAG"
|
|
||||||
echo ""
|
|
||||||
echo "DEB dists/stable/:"
|
|
||||||
aws s3 ls "s3://${R2_BUCKET}/deb/dists/stable/" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" 2>/dev/null || echo " (empty)"
|
|
||||||
echo "DEB pool/main/:"
|
|
||||||
aws s3 ls "s3://${R2_BUCKET}/deb/pool/main/" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" 2>/dev/null || echo " (empty)"
|
|
||||||
echo "RPM repodata/:"
|
|
||||||
aws s3 ls "s3://${R2_BUCKET}/rpm/repodata/" \
|
|
||||||
--endpoint-url "$R2_ENDPOINT" 2>/dev/null || echo " (empty)"
|
|
||||||
|
|||||||
@@ -246,7 +246,12 @@ jobs:
|
|||||||
|
|
||||||
# Copy sidecar binaries
|
# Copy sidecar binaries
|
||||||
cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/"
|
cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/"
|
||||||
cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/"
|
# The daemon is currently disabled (no Cargo bin target), so it isn't
|
||||||
|
# built. Copy it only if a build produced it, so the absent binary
|
||||||
|
# doesn't fail the job.
|
||||||
|
if [ -f "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" ]; then
|
||||||
|
cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
# Copy WebView2Loader if present
|
# Copy WebView2Loader if present
|
||||||
if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then
|
if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then
|
||||||
|
|||||||
@@ -247,7 +247,12 @@ jobs:
|
|||||||
|
|
||||||
cp "src-tauri/target/${{ matrix.target }}/release/donutbrowser.exe" "$PORTABLE_DIR/Donut.exe"
|
cp "src-tauri/target/${{ matrix.target }}/release/donutbrowser.exe" "$PORTABLE_DIR/Donut.exe"
|
||||||
cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/"
|
cp "src-tauri/target/${{ matrix.target }}/release/donut-proxy.exe" "$PORTABLE_DIR/"
|
||||||
cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/"
|
# The daemon is currently disabled (no Cargo bin target), so it isn't
|
||||||
|
# built. Copy it only if a build produced it, so the absent binary
|
||||||
|
# doesn't fail the job.
|
||||||
|
if [ -f "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" ]; then
|
||||||
|
cp "src-tauri/target/${{ matrix.target }}/release/donut-daemon.exe" "$PORTABLE_DIR/"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then
|
if [ -f "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" ]; then
|
||||||
cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/"
|
cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ donutbrowser/
|
|||||||
│ ├── app/ # App router (page.tsx, layout.tsx)
|
│ ├── app/ # App router (page.tsx, layout.tsx)
|
||||||
│ ├── components/ # 50+ React components (dialogs, tables, UI)
|
│ ├── components/ # 50+ React components (dialogs, tables, UI)
|
||||||
│ ├── hooks/ # Event-driven React hooks
|
│ ├── hooks/ # Event-driven React hooks
|
||||||
│ ├── i18n/locales/ # Translations (en, es, fr, ja, pt, ru, zh)
|
│ ├── i18n/locales/ # Translations (en, es, fr, ja, ko, pt, ru, vi, zh)
|
||||||
│ ├── lib/ # Utilities (themes, toast, browser-utils)
|
│ ├── lib/ # Utilities (themes, toast, browser-utils)
|
||||||
│ └── types.ts # Shared TypeScript interfaces
|
│ └── types.ts # Shared TypeScript interfaces
|
||||||
├── src-tauri/ # Rust backend (Tauri)
|
├── src-tauri/ # Rust backend (Tauri)
|
||||||
@@ -76,12 +76,12 @@ Linux/Windows swap `~/Library/Logs/com.donutbrowser/` for the platform-appropria
|
|||||||
|
|
||||||
- Never write user-facing strings as raw English literals in JSX, toast messages, dialog titles/descriptions, button labels, placeholders, table headers, tooltips, or empty-state text. Always go through `t("namespace.key")` from `useTranslation()`.
|
- Never write user-facing strings as raw English literals in JSX, toast messages, dialog titles/descriptions, button labels, placeholders, table headers, tooltips, or empty-state text. Always go through `t("namespace.key")` from `useTranslation()`.
|
||||||
- This applies to every component under `src/` — including new ones. If a component doesn't already import `useTranslation`, add it.
|
- This applies to every component under `src/` — including new ones. If a component doesn't already import `useTranslation`, add it.
|
||||||
- Adding a new string means adding the key to ALL seven locale files in `src/i18n/locales/` (en, es, fr, ja, pt, ru, zh) — not just `en.json`. The English version alone is incomplete work.
|
- Adding a new string means adding the key to ALL nine locale files in `src/i18n/locales/` (en, es, fr, ja, ko, pt, ru, vi, zh) — not just `en.json`. The English version alone is incomplete work.
|
||||||
- Reuse existing keys (`common.buttons.*`, `common.labels.*`, `createProfile.*`, etc.) before creating new namespaces. Check `en.json` first.
|
- Reuse existing keys (`common.buttons.*`, `common.labels.*`, `createProfile.*`, etc.) before creating new namespaces. Check `en.json` first.
|
||||||
- Strings excluded from this rule: `console.log/warn/error`, dev-only debug labels, internal IDs, CSS class names, type names. If unsure whether a string renders to the user, assume it does and translate it.
|
- Strings excluded from this rule: `console.log/warn/error`, dev-only debug labels, internal IDs, CSS class names, type names. If unsure whether a string renders to the user, assume it does and translate it.
|
||||||
- **Never use `t(key, "fallback")` with a default-value second argument.** The 2-arg form is forbidden — every key must exist in every locale file before the call site lands. Fallbacks mask missing translations: a key missing from `ru.json` will silently render the English fallback to Russian users, so the bug never surfaces in CI or review. Only call `t("namespace.key")`. If a translation is missing for any locale, that's a bug to fix at the JSON, not a hole to paper over at the call site.
|
- **Never use `t(key, "fallback")` with a default-value second argument.** The 2-arg form is forbidden — every key must exist in every locale file before the call site lands. Fallbacks mask missing translations: a key missing from `ru.json` will silently render the English fallback to Russian users, so the bug never surfaces in CI or review. Only call `t("namespace.key")`. If a translation is missing for any locale, that's a bug to fix at the JSON, not a hole to paper over at the call site.
|
||||||
- Empty-string values in non-English locales are also forbidden — a locale either has the right translation or it has the same content as English; never `""`. If a particular language doesn't need a particular phrase (e.g. a suffix that doesn't grammatically apply), refactor the JSX to use a single interpolated key (`t("foo.bar", { name })` with `"...{{name}}..."` in each locale) instead of splitting prefix/suffix.
|
- Empty-string values in non-English locales are also forbidden — a locale either has the right translation or it has the same content as English; never `""`. If a particular language doesn't need a particular phrase (e.g. a suffix that doesn't grammatically apply), refactor the JSX to use a single interpolated key (`t("foo.bar", { name })` with `"...{{name}}..."` in each locale) instead of splitting prefix/suffix.
|
||||||
- When adding or removing keys across all seven locales, use a one-shot Python script in `/tmp/` that loads each `*.json`, mutates it, and writes it back. Seven sequential `Edit` calls drift (typos, ordering differences) and burn tokens; a single script keeps the locales in lockstep and is easy to throw away.
|
- When adding or removing keys across all nine locales, use a one-shot Python script in `/tmp/` that loads each `*.json`, mutates it, and writes it back. Nine sequential `Edit` calls drift (typos, ordering differences) and burn tokens; a single script keeps the locales in lockstep and is easy to throw away.
|
||||||
|
|
||||||
## Backend error codes (mandatory)
|
## Backend error codes (mandatory)
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ User-facing errors returned from a Tauri command MUST be JSON `{ "code": "FOO_BA
|
|||||||
```
|
```
|
||||||
2. Add `"FOO_BAR"` to the `BackendErrorCode` union in `src/lib/backend-errors.ts`.
|
2. Add `"FOO_BAR"` to the `BackendErrorCode` union in `src/lib/backend-errors.ts`.
|
||||||
3. Add a `case "FOO_BAR":` in the switch that returns `t("backendErrors.fooBar", …)`.
|
3. Add a `case "FOO_BAR":` in the switch that returns `t("backendErrors.fooBar", …)`.
|
||||||
4. Add `backendErrors.fooBar` to all seven locale files.
|
4. Add `backendErrors.fooBar` to all nine locale files.
|
||||||
|
|
||||||
Raw error strings reach the user untranslated; that's the bug pattern this rule blocks.
|
Raw error strings reach the user untranslated; that's the bug pattern this rule blocks.
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ Reference implementations: `proxy-management-dialog.tsx`, `extension-management-
|
|||||||
|
|
||||||
All app-wide shortcuts live in `src/lib/shortcuts.ts`:
|
All app-wide shortcuts live in `src/lib/shortcuts.ts`:
|
||||||
|
|
||||||
- `SHORTCUTS[]` — one entry per shortcut (id, label translation key, group, key, modifier flags). The label key must exist in all seven locales.
|
- `SHORTCUTS[]` — one entry per shortcut (id, label translation key, group, key, modifier flags). The label key must exist in all nine locales.
|
||||||
- `formatShortcut(s)` returns platform-correct token strings (`["⌘", "K"]` on mac, `["Ctrl", "K"]` elsewhere) — used by both the shortcuts page and the command palette.
|
- `formatShortcut(s)` returns platform-correct token strings (`["⌘", "K"]` on mac, `["Ctrl", "K"]` elsewhere) — used by both the shortcuts page and the command palette.
|
||||||
- `matchesShortcut(s, event)` matches a real `KeyboardEvent` and rejects the wrong-platform modifier so Ctrl+K on macOS never fires a `mod: true` shortcut.
|
- `matchesShortcut(s, event)` matches a real `KeyboardEvent` and rejects the wrong-platform modifier so Ctrl+K on macOS never fires a `mod: true` shortcut.
|
||||||
- `matchesGroupDigit(event)` returns 1–9 if Mod+digit was pressed — group switching is dynamic (driven by `orderedGroupTargets` in `page.tsx`) and isn't in the `SHORTCUTS` table.
|
- `matchesGroupDigit(event)` returns 1–9 if Mod+digit was pressed — group switching is dynamic (driven by `orderedGroupTargets` in `page.tsx`) and isn't in the `SHORTCUTS` table.
|
||||||
@@ -158,7 +158,7 @@ Dispatch: the global `keydown` listener and the `runShortcut` callback both live
|
|||||||
1. Append to `SHORTCUTS` in `src/lib/shortcuts.ts`. Add the `ShortcutId` variant.
|
1. Append to `SHORTCUTS` in `src/lib/shortcuts.ts`. Add the `ShortcutId` variant.
|
||||||
2. Add a `case "yourId":` in `runShortcut` in `page.tsx`.
|
2. Add a `case "yourId":` in `runShortcut` in `page.tsx`.
|
||||||
3. Add the icon mapping in `src/components/command-palette.tsx::ICONS`.
|
3. Add the icon mapping in `src/components/command-palette.tsx::ICONS`.
|
||||||
4. Add `shortcuts.yourId` (label) to all seven locale files.
|
4. Add `shortcuts.yourId` (label) to all nine locale files.
|
||||||
|
|
||||||
The command palette (Mod+K) is built on the shadcn `Command` primitive with a token-AND fuzzy filter — `fuzzyFilter` in `command-palette.tsx`. The `CommandDialog` wrapper now forwards `filter`/`shouldFilter` to the inner `Command` for callers that need custom matching.
|
The command palette (Mod+K) is built on the shadcn `Command` primitive with a token-AND fuzzy filter — `fuzzyFilter` in `command-palette.tsx`. The `CommandDialog` wrapper now forwards `filter`/`shouldFilter` to the inner `Command` for callers that need custom matching.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
|
||||||
|
## v0.25.0 (2026-06-01)
|
||||||
|
|
||||||
|
Note: created manually due to CI issue
|
||||||
|
|
||||||
|
- Onboarding added for new users.
|
||||||
|
- When closing the window, you can choose to minimize to tray or quit.
|
||||||
|
- Improved feedback for macOS permission grants.
|
||||||
|
- Cloud login now opens in your external browser.
|
||||||
|
|
||||||
## v0.24.4 (2026-05-26)
|
## v0.24.4 (2026-05-26)
|
||||||
|
|
||||||
### Refactoring
|
### Refactoring
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ codeql database analyze /tmp/codeql-rust --format=sarifv2.1.0 --output=/tmp/rust
|
|||||||
|
|
||||||
## Key Rules
|
## Key Rules
|
||||||
|
|
||||||
- **Translations**: Any UI text changes must be reflected in all 7 locale files (`src/i18n/locales/`)
|
- **Translations**: Any UI text changes must be reflected in all 9 locale files (`src/i18n/locales/`)
|
||||||
- **Tauri commands**: If you modify Tauri commands, the `test_no_unused_tauri_commands` test will catch unused ones
|
- **Tauri commands**: If you modify Tauri commands, the `test_no_unused_tauri_commands` test will catch unused ones
|
||||||
- **No hardcoded colors**: Use theme CSS variables (see `src/lib/themes.ts`), never Tailwind color classes like `text-red-500`
|
- **No hardcoded colors**: Use theme CSS variables (see `src/lib/themes.ts`), never Tailwind color classes like `text-red-500`
|
||||||
- **No lock file changes**: Don't update `pnpm-lock.yaml` or `Cargo.lock` unless updating dependencies is the purpose of the PR
|
- **No lock file changes**: Don't update `pnpm-lock.yaml` or `Cargo.lock` unless updating dependencies is the purpose of the PR
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "donutbrowser",
|
"name": "donutbrowser",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 12341",
|
"dev": "next dev --turbopack -p 12341",
|
||||||
|
|||||||
Generated
+1
-1
@@ -1784,7 +1784,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donutbrowser"
|
name = "donutbrowser"
|
||||||
version = "0.25.0"
|
version = "0.25.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes 0.9.1",
|
"aes 0.9.1",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "donutbrowser"
|
name = "donutbrowser"
|
||||||
version = "0.25.0"
|
version = "0.25.1"
|
||||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||||
authors = ["zhom@github"]
|
authors = ["zhom@github"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Donut",
|
"productName": "Donut",
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"identifier": "com.donutbrowser",
|
"identifier": "com.donutbrowser",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
|
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
|
||||||
|
|||||||
@@ -483,7 +483,8 @@ export function SettingsDialog({
|
|||||||
| "zh"
|
| "zh"
|
||||||
| "ja"
|
| "ja"
|
||||||
| "ko"
|
| "ko"
|
||||||
| "ru"),
|
| "ru"
|
||||||
|
| "vi"),
|
||||||
);
|
);
|
||||||
setOriginalLanguage(selectedLanguage);
|
setOriginalLanguage(selectedLanguage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ja from "./locales/ja.json";
|
|||||||
import ko from "./locales/ko.json";
|
import ko from "./locales/ko.json";
|
||||||
import pt from "./locales/pt.json";
|
import pt from "./locales/pt.json";
|
||||||
import ru from "./locales/ru.json";
|
import ru from "./locales/ru.json";
|
||||||
|
import vi from "./locales/vi.json";
|
||||||
import zh from "./locales/zh.json";
|
import zh from "./locales/zh.json";
|
||||||
|
|
||||||
export const SUPPORTED_LANGUAGES = [
|
export const SUPPORTED_LANGUAGES = [
|
||||||
@@ -19,6 +20,7 @@ export const SUPPORTED_LANGUAGES = [
|
|||||||
{ code: "ja", name: "Japanese", nativeName: "日本語" },
|
{ code: "ja", name: "Japanese", nativeName: "日本語" },
|
||||||
{ code: "ko", name: "Korean", nativeName: "한국어" },
|
{ code: "ko", name: "Korean", nativeName: "한국어" },
|
||||||
{ code: "ru", name: "Russian", nativeName: "Русский" },
|
{ code: "ru", name: "Russian", nativeName: "Русский" },
|
||||||
|
{ code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
||||||
@@ -36,6 +38,7 @@ export const LANGUAGE_FALLBACKS: Record<string, string[]> = {
|
|||||||
"es-ES": ["es", "en"],
|
"es-ES": ["es", "en"],
|
||||||
"fr-CA": ["fr", "en"],
|
"fr-CA": ["fr", "en"],
|
||||||
"fr-FR": ["fr", "en"],
|
"fr-FR": ["fr", "en"],
|
||||||
|
"vi-VN": ["vi", "en"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLanguageWithFallback(systemLocale: string): string {
|
export function getLanguageWithFallback(systemLocale: string): string {
|
||||||
@@ -65,6 +68,7 @@ const resources = {
|
|||||||
ja: { translation: ja },
|
ja: { translation: ja },
|
||||||
ko: { translation: ko },
|
ko: { translation: ko },
|
||||||
ru: { translation: ru },
|
ru: { translation: ru },
|
||||||
|
vi: { translation: vi },
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n.use(initReactI18next).init({
|
i18n.use(initReactI18next).init({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user