mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-11 17:27:54 +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:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,7 +13,12 @@ env:
|
||||
|
||||
jobs:
|
||||
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
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -44,7 +49,7 @@ jobs:
|
||||
- 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.
|
||||
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
|
||||
{
|
||||
@@ -83,7 +88,7 @@ jobs:
|
||||
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/raw.txt
|
||||
|
||||
# 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
|
||||
if ! jq -e . /tmp/result.json >/dev/null 2>&1; then
|
||||
echo "::warning::Model returned non-JSON; treating as compliant"
|
||||
@@ -94,6 +99,7 @@ jobs:
|
||||
cat /tmp/result.json
|
||||
|
||||
- name: Build comment
|
||||
id: build
|
||||
run: |
|
||||
python3 - <<'EOF'
|
||||
import json, os
|
||||
@@ -103,167 +109,25 @@ jobs:
|
||||
|
||||
parts = []
|
||||
if not compliant:
|
||||
parts.append('<!-- issue-compliance -->')
|
||||
parts.append("This issue doesn't fully meet our [contributing guidelines](../blob/main/CONTRIBUTING.md).")
|
||||
parts.append("This issue was automatically closed because it doesn't follow our [issue templates](../issues/new/choose).")
|
||||
parts.append('')
|
||||
parts.append('**What needs to be fixed:**')
|
||||
parts.append('**What was missing:**')
|
||||
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.')
|
||||
parts.append('')
|
||||
parts.append('If you believe this was flagged incorrectly, please let a maintainer know.')
|
||||
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.')
|
||||
|
||||
comment = '\n'.join(parts).strip()
|
||||
open('/tmp/comment.md', 'w').write(comment)
|
||||
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')
|
||||
EOF
|
||||
id: build
|
||||
|
||||
- name: Post comment
|
||||
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
|
||||
- name: Comment and close non-compliant issue
|
||||
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:
|
||||
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
|
||||
gh issue close "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --reason "not planned"
|
||||
|
||||
@@ -23,6 +23,9 @@ jobs:
|
||||
github.event.workflow_run.conclusion == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
|
||||
- name: Determine release tag
|
||||
id: tag
|
||||
env:
|
||||
@@ -40,182 +43,32 @@ jobs:
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Configure aws-cli for R2
|
||||
# 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
|
||||
- name: Install tools (dpkg-dev, createrepo-c, aws-cli v1)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dpkg-dev createrepo-c python3-pip
|
||||
# Remove pre-installed aws-cli v2 — it sends CRC64NVME checksums
|
||||
# that Cloudflare R2 rejects with Unauthorized, and the s3transfer
|
||||
# lib has a confirmed bug where WHEN_REQUIRED is silently ignored
|
||||
# (boto/s3transfer#327). Install aws-cli v1 via pip instead.
|
||||
# GitHub runners ship aws-cli v2, which sends CRC64NVME integrity
|
||||
# checksums that Cloudflare R2 rejects with `Unauthorized` on
|
||||
# ListObjectsV2 (the call behind `aws s3 sync`). aws-cli v1 predates
|
||||
# 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 -rf /usr/local/aws-cli
|
||||
pip3 install --break-system-packages awscli
|
||||
# Ensure pip-installed aws is on PATH (pip may install to ~/.local/bin)
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
sudo pip3 install --break-system-packages 'awscli<2'
|
||||
hash -r
|
||||
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:
|
||||
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 }}
|
||||
TAG: ${{ 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)"
|
||||
run: bash scripts/publish-repo.sh "${{ steps.tag.outputs.tag }}"
|
||||
|
||||
@@ -246,7 +246,12 @@ jobs:
|
||||
|
||||
# Copy sidecar binaries
|
||||
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
|
||||
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/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
|
||||
cp "src-tauri/target/${{ matrix.target }}/release/WebView2Loader.dll" "$PORTABLE_DIR/"
|
||||
|
||||
@@ -11,7 +11,7 @@ donutbrowser/
|
||||
│ ├── app/ # App router (page.tsx, layout.tsx)
|
||||
│ ├── components/ # 50+ React components (dialogs, tables, UI)
|
||||
│ ├── 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)
|
||||
│ └── types.ts # Shared TypeScript interfaces
|
||||
├── 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()`.
|
||||
- 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.
|
||||
- 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.
|
||||
- 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)
|
||||
|
||||
@@ -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`.
|
||||
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.
|
||||
|
||||
@@ -148,7 +148,7 @@ Reference implementations: `proxy-management-dialog.tsx`, `extension-management-
|
||||
|
||||
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.
|
||||
- `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.
|
||||
@@ -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.
|
||||
2. Add a `case "yourId":` in `runShortcut` in `page.tsx`.
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
# 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)
|
||||
|
||||
### Refactoring
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ codeql database analyze /tmp/codeql-rust --format=sarifv2.1.0 --output=/tmp/rust
|
||||
|
||||
## 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
|
||||
- **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
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 12341",
|
||||
|
||||
Generated
+1
-1
@@ -1784,7 +1784,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.25.0"
|
||||
version = "0.25.1"
|
||||
dependencies = [
|
||||
"aes 0.9.1",
|
||||
"aes-gcm",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.25.0"
|
||||
version = "0.25.1"
|
||||
description = "Simple Yet Powerful Anti-Detect Browser"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut",
|
||||
"version": "0.25.0",
|
||||
"version": "0.25.1",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
|
||||
|
||||
@@ -483,7 +483,8 @@ export function SettingsDialog({
|
||||
| "zh"
|
||||
| "ja"
|
||||
| "ko"
|
||||
| "ru"),
|
||||
| "ru"
|
||||
| "vi"),
|
||||
);
|
||||
setOriginalLanguage(selectedLanguage);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import ja from "./locales/ja.json";
|
||||
import ko from "./locales/ko.json";
|
||||
import pt from "./locales/pt.json";
|
||||
import ru from "./locales/ru.json";
|
||||
import vi from "./locales/vi.json";
|
||||
import zh from "./locales/zh.json";
|
||||
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
@@ -19,6 +20,7 @@ export const SUPPORTED_LANGUAGES = [
|
||||
{ code: "ja", name: "Japanese", nativeName: "日本語" },
|
||||
{ code: "ko", name: "Korean", nativeName: "한국어" },
|
||||
{ code: "ru", name: "Russian", nativeName: "Русский" },
|
||||
{ code: "vi", name: "Vietnamese", nativeName: "Tiếng Việt" },
|
||||
] as const;
|
||||
|
||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]["code"];
|
||||
@@ -36,6 +38,7 @@ export const LANGUAGE_FALLBACKS: Record<string, string[]> = {
|
||||
"es-ES": ["es", "en"],
|
||||
"fr-CA": ["fr", "en"],
|
||||
"fr-FR": ["fr", "en"],
|
||||
"vi-VN": ["vi", "en"],
|
||||
};
|
||||
|
||||
export function getLanguageWithFallback(systemLocale: string): string {
|
||||
@@ -65,6 +68,7 @@ const resources = {
|
||||
ja: { translation: ja },
|
||||
ko: { translation: ko },
|
||||
ru: { translation: ru },
|
||||
vi: { translation: vi },
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user