mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 09:47:51 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e006d56387 | |||
| 43f9f02029 | |||
| 839265de35 | |||
| 0d85b61c96 |
@@ -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/"
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
+120
-21
@@ -33,7 +33,8 @@
|
||||
"minimize": "Thu nhỏ",
|
||||
"saving": "Đang lưu…",
|
||||
"saved": "Đã lưu",
|
||||
"copied": "Đã sao chép"
|
||||
"copied": "Đã sao chép",
|
||||
"learnMore": "Tìm hiểu thêm"
|
||||
},
|
||||
"status": {
|
||||
"active": "Đang hoạt động",
|
||||
@@ -99,6 +100,9 @@
|
||||
"srOnly": {
|
||||
"copy": "Sao chép",
|
||||
"copied": "Đã sao chép"
|
||||
},
|
||||
"placeholders": {
|
||||
"example": "ví dụ: {{value}}"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
@@ -340,7 +344,8 @@
|
||||
"downloading": "Đang tải xuống {{browser}} phiên bản ({{version}})...",
|
||||
"latestNeedsDownload": "Phiên bản mới nhất ({{version}}) cần được tải xuống",
|
||||
"latestAvailable": "Phiên bản mới nhất ({{version}}) đã sẵn sàng",
|
||||
"latestDownloading": "Đang tải xuống phiên bản ({{version}})..."
|
||||
"latestDownloading": "Đang tải xuống phiên bản ({{version}})...",
|
||||
"upgradeAvailable": "Đã có phiên bản mới hơn ({{version}}) của {{browser}}."
|
||||
},
|
||||
"chromiumLabel": "Chromium",
|
||||
"chromiumSubtitle": "Dựa trên Wayfern",
|
||||
@@ -351,7 +356,9 @@
|
||||
"passwordProtect": {
|
||||
"label": "Bảo vệ hồ sơ bằng mật khẩu",
|
||||
"description": "Mã hóa dữ liệu hồ sơ trên ổ đĩa. Yêu cầu mật khẩu để khởi chạy."
|
||||
}
|
||||
},
|
||||
"downloadingSubtitle": "Đang tải xuống…",
|
||||
"browsersDownloading": "Trình duyệt vẫn đang được tải xuống. Việc tạo hồ sơ sẽ khả dụng khi quá trình tải hoàn tất."
|
||||
},
|
||||
"deleteDialog": {
|
||||
"title": "Xóa hồ sơ",
|
||||
@@ -1188,7 +1195,9 @@
|
||||
"cannotWhileRunning": "Dừng profile trước khi thay đổi mật khẩu."
|
||||
},
|
||||
"fingerprint": {
|
||||
"notSupported": "Chỉnh sửa vân tay chỉ khả dụng cho profile Camoufox và Wayfern."
|
||||
"notSupported": "Chỉnh sửa vân tay chỉ khả dụng cho profile Camoufox và Wayfern.",
|
||||
"lockedTitle": "Vân tay là tính năng Pro",
|
||||
"lockedDescription": "Xem và chỉnh sửa vân tay của profile yêu cầu gói trả phí đang hoạt động. Nâng cấp để mở khóa bảo vệ vân tay."
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
@@ -1524,17 +1533,6 @@
|
||||
"creatingButton": "Đang tạo...",
|
||||
"createButton": "Tạo"
|
||||
},
|
||||
"launchOnLogin": {
|
||||
"title": "Bật khởi chạy khi đăng nhập?",
|
||||
"description": "Chạy trong nền giúp giữ proxy và trình duyệt của bạn hoạt động.",
|
||||
"declineButton": "Không hỏi lại",
|
||||
"declining": "...",
|
||||
"enableButton": "Bật",
|
||||
"enableSuccess": "Đã bật khởi chạy khi đăng nhập",
|
||||
"enableFailed": "Bật khởi chạy khi đăng nhập thất bại",
|
||||
"declineFailed": "Lưu tùy chọn thất bại",
|
||||
"tryAgain": "Vui lòng thử lại"
|
||||
},
|
||||
"wayfernTerms": {
|
||||
"title": "Điều khoản và điều kiện Wayfern",
|
||||
"description": "Trước khi sử dụng Donut Browser, bạn phải đọc và đồng ý với Điều khoản và Điều kiện của Wayfern.",
|
||||
@@ -1680,7 +1678,8 @@
|
||||
"viewRelease": "Xem phiên bản",
|
||||
"later": "Để sau",
|
||||
"uploading": "Đang tải lên",
|
||||
"downloading": "Đang tải xuống"
|
||||
"downloading": "Đang tải xuống",
|
||||
"startingUpdate": "Đang bắt đầu cập nhật..."
|
||||
}
|
||||
},
|
||||
"browserDownload": {
|
||||
@@ -1694,7 +1693,9 @@
|
||||
"extractionFailedDescription": "Tệp hỏng đã bị xóa. Sẽ được tải lại trong lần thử tiếp theo.",
|
||||
"extracting": "Đang giải nén tệp trình duyệt... Vui lòng không đóng ứng dụng.",
|
||||
"verifying": "Đang xác minh tệp trình duyệt...",
|
||||
"downloadingRolling": "Đang tải bản phát hành rolling..."
|
||||
"downloadingRolling": "Đang tải bản phát hành rolling...",
|
||||
"geoipDownloading": "Đang tải cơ sở dữ liệu GeoIP",
|
||||
"geoipDownloaded": "Đã tải cơ sở dữ liệu GeoIP thành công!"
|
||||
}
|
||||
},
|
||||
"versionUpdater": {
|
||||
@@ -1712,7 +1713,12 @@
|
||||
"updateSuccessDescription": "Tìm thấy {{newVersions}} phiên bản mới trên {{successfulUpdates}} trình duyệt. Tự động tải xuống sẽ bắt đầu sớm.",
|
||||
"upToDate": "Không tìm thấy phiên bản trình duyệt mới",
|
||||
"upToDateDescription": "Tất cả phiên bản trình duyệt đều đã cập nhật",
|
||||
"updateAllFailed": "Cập nhật phiên bản trình duyệt thất bại"
|
||||
"updateAllFailed": "Cập nhật phiên bản trình duyệt thất bại",
|
||||
"updateStarted": "Đã bắt đầu cập nhật {{browser}}",
|
||||
"updateStartedDescription": "Quá trình tải phiên bản {{version}} sẽ sớm bắt đầu. Việc khởi chạy trình duyệt bị tắt cho đến khi cập nhật hoàn tất.",
|
||||
"downloadStarting": "Đang bắt đầu tải {{browser}} {{version}}",
|
||||
"downloadProgressBelow": "Tiến trình tải sẽ hiển thị bên dưới...",
|
||||
"autoDownloadStarted": "Đang tự động tải {{browser}} {{version}}. Tiến trình sẽ hiển thị bên dưới."
|
||||
}
|
||||
},
|
||||
"profilePassword": {
|
||||
@@ -1800,7 +1806,11 @@
|
||||
"invalidLaunchHookUrl": "URL hook khởi chạy không hợp lệ. Sử dụng URL http:// hoặc https:// đầy đủ.",
|
||||
"cookieDbLocked": "Không thể đọc cookie — cơ sở dữ liệu bị khóa. Đóng trình duyệt và thử lại.",
|
||||
"cookieDbUnavailable": "Không thể đọc cookie — kho cookie không khả dụng.",
|
||||
"selfHostedRequiresLogout": "Đăng xuất khỏi tài khoản Donut trước khi cấu hình máy chủ tự lưu trữ."
|
||||
"selfHostedRequiresLogout": "Đăng xuất khỏi tài khoản Donut trước khi cấu hình máy chủ tự lưu trữ.",
|
||||
"fingerprintRequiresPro": "Bảo vệ vân tay yêu cầu gói trả phí đang hoạt động.",
|
||||
"proxyNotWorking": "Proxy đã chọn không hoạt động, nên profile chưa được tạo.",
|
||||
"proxyPaymentRequired": "Proxy đã chọn yêu cầu thanh toán (402) — gói đăng ký của nó có thể đã hết hạn — nên profile chưa được tạo.",
|
||||
"vpnNotWorking": "VPN đã chọn không hoạt động, nên profile chưa được tạo."
|
||||
},
|
||||
"rail": {
|
||||
"profiles": "Profile",
|
||||
@@ -1866,7 +1876,8 @@
|
||||
"plan": "Gói",
|
||||
"status": "Trạng thái",
|
||||
"teamRole": "Vai trò nhóm",
|
||||
"period": "Chu kỳ thanh toán"
|
||||
"period": "Chu kỳ thanh toán",
|
||||
"device": "Thiết bị"
|
||||
},
|
||||
"tabs": {
|
||||
"account": "Tài khoản",
|
||||
@@ -1880,7 +1891,10 @@
|
||||
"statusUnknown": "Chưa kiểm tra",
|
||||
"testConnection": "Kiểm tra kết nối",
|
||||
"disconnect": "Ngắt kết nối"
|
||||
}
|
||||
},
|
||||
"deviceOrdinal": "{{ordinal}} trên {{count}}",
|
||||
"automationPrimaryOnly": "Tự động hóa trình duyệt chỉ chạy trên thiết bị chính của bạn (Thiết bị 1). Đăng xuất ở đó để sử dụng tại đây.",
|
||||
"automationActiveHere": "Tự động hóa trình duyệt đang hoạt động trên thiết bị này."
|
||||
},
|
||||
"shortcutsPage": {
|
||||
"title": "Phím tắt",
|
||||
@@ -1918,5 +1932,90 @@
|
||||
"description": "Bạn muốn thu nhỏ ứng dụng vào khay hệ thống hay thoát?",
|
||||
"minimize": "Thu nhỏ vào khay",
|
||||
"quit": "Thoát"
|
||||
},
|
||||
"tray": {
|
||||
"show": "Hiển thị Donut Browser",
|
||||
"quit": "Thoát"
|
||||
},
|
||||
"browserSupport": {
|
||||
"endingSoonTitle": "Hỗ trợ trình duyệt sắp kết thúc",
|
||||
"endingSoonDescription": "Hỗ trợ cho các profile sau sẽ bị gỡ bỏ vào ngày 15 tháng 3 năm 2026: {{profiles}}. Vui lòng chuyển sang profile Wayfern hoặc Camoufox."
|
||||
},
|
||||
"onboarding": {
|
||||
"steps": {
|
||||
"createProfile": {
|
||||
"title": "Tạo hồ sơ đầu tiên của bạn",
|
||||
"content": "Nhấn vào đây để tạo hồ sơ đầu tiên của bạn. Chọn Wayfern làm trình duyệt — Chromium được khuyến nghị, có bảo vệ vân tay."
|
||||
},
|
||||
"dnsBlocking": {
|
||||
"title": "Chặn DNS",
|
||||
"content": "Dùng menu thả xuống này để đặt mức danh sách chặn DNS cho hồ sơ — nó chặn quảng cáo, trình theo dõi và phần mềm độc hại ở cấp mạng. Mức cao hơn chặn nhiều hơn."
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"skip": "Bỏ qua",
|
||||
"back": "Quay lại",
|
||||
"next": "Tiếp theo",
|
||||
"finish": "Hoàn tất"
|
||||
},
|
||||
"thankYou": {
|
||||
"title": "Cảm ơn bạn đã chọn Donut Browser",
|
||||
"body": "Hy vọng nó giúp việc duyệt web của bạn riêng tư hơn — mỗi danh tính được giữ riêng, và không có gì rời khỏi máy của bạn. Chúc bạn duyệt web vui vẻ.",
|
||||
"cta": "Bắt đầu duyệt web"
|
||||
}
|
||||
},
|
||||
"welcome": {
|
||||
"title": "Chào mừng đến với Donut Browser",
|
||||
"tagline": "Trình duyệt chống phát hiện mã nguồn mở để quản lý nhiều danh tính cùng lúc.",
|
||||
"skip": "Bỏ qua",
|
||||
"next": "Tiếp theo",
|
||||
"permissions": {
|
||||
"title": "Cho phép micro & camera",
|
||||
"desc": "Cấp quyền truy cập để các trang cần micro hoặc camera hoạt động bên trong các hồ sơ trình duyệt của bạn. macOS chỉ hỏi một lần; mỗi trang vẫn hỏi riêng bạn.",
|
||||
"skip": "Để sau",
|
||||
"grant": "Cho phép truy cập",
|
||||
"requesting": "Đang yêu cầu…"
|
||||
},
|
||||
"ready": {
|
||||
"title": "Đang thiết lập",
|
||||
"descDownloading": "Đang tải trình duyệt đầu tiên của bạn (Wayfern). Quá trình thiết lập một lần này chạy ngầm — vui lòng chờ.",
|
||||
"descReady": "Trình duyệt của bạn đã sẵn sàng. Hãy tạo hồ sơ đầu tiên của bạn.",
|
||||
"cta": "Tạo hồ sơ đầu tiên của tôi",
|
||||
"downloading": "Đang tải xuống…",
|
||||
"extracting": "Đang giải nén…",
|
||||
"stats": "{{downloaded}} trên {{total}}",
|
||||
"speed": "{{speed}}/giây",
|
||||
"timeLeft": "còn {{time}}",
|
||||
"descExtracting": "Đang giải nén trình duyệt của bạn. Quá trình thiết lập một lần này chạy ngầm — vui lòng chờ.",
|
||||
"almostFinished": "Sắp hoàn tất…",
|
||||
"errorTitle": "Thiết lập thất bại",
|
||||
"errorDownload": "Không thể tải {{browser}}. Kiểm tra kết nối và thử lại.",
|
||||
"errorExtraction": "Không thể giải nén {{browser}}. Vui lòng thử lại.",
|
||||
"errorGeneric": "Đã xảy ra lỗi khi thiết lập {{browser}}. Vui lòng thử lại.",
|
||||
"retry": "Thử lại"
|
||||
},
|
||||
"features": {
|
||||
"title": "Tính năng",
|
||||
"items": {
|
||||
"setDefault": "Đặt làm trình duyệt mặc định",
|
||||
"proxy": "Hỗ trợ Proxy (HTTP/SOCKS5)",
|
||||
"vpn": "Hỗ trợ VPN (WireGuard)",
|
||||
"profiles": "Hồ sơ cục bộ không giới hạn",
|
||||
"api": "API quản lý hồ sơ & MCP",
|
||||
"openSource": "Mã nguồn mở",
|
||||
"groups": "Nhóm hồ sơ",
|
||||
"cookies": "Nhập & xuất cookie"
|
||||
}
|
||||
},
|
||||
"license": {
|
||||
"title": "Cấp phép",
|
||||
"body": "Donut Browser là mã nguồn mở và miễn phí sử dụng.",
|
||||
"agree": "Tôi hiểu",
|
||||
"personalTitle": "Sử dụng cá nhân",
|
||||
"personalDesc": "Miễn phí vĩnh viễn.",
|
||||
"commercialTitle": "Sử dụng thương mại",
|
||||
"trialBadge": "2 tuần miễn phí",
|
||||
"commercialDesc": "Miễn phí dùng thử 2 tuần. Sau đó, gói trả phí giúp dự án được duy trì và phát triển."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user