chore: sync dev to master

This commit is contained in:
github-actions[bot]
2026-05-27 13:25:04 +00:00
3 changed files with 232 additions and 68 deletions

View File

@@ -16,7 +16,17 @@ on:
branches:
- master
- dev
workflow_dispatch: {}
workflow_dispatch:
inputs:
release_tag:
description: "Existing release tag to rebuild, e.g. v1.2.3"
required: false
type: string
overwrite_existing_release:
description: "Overwrite assets/body of an existing GitHub Release"
required: false
default: false
type: boolean
schedule:
- cron: '0 19 * * 0'
@@ -53,12 +63,44 @@ jobs:
- name: Determine version
id: version
env:
DISPATCH_RELEASE_TAG: ${{ inputs.release_tag }}
OVERWRITE_EXISTING_RELEASE: ${{ inputs.overwrite_existing_release }}
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
echo "Detected version from tag: $VERSION"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" && "$OVERWRITE_EXISTING_RELEASE" == "true" ]]; then
git fetch --tags --force origin
VERSION="${DISPATCH_RELEASE_TAG:-}"
VERSION="${VERSION#"${VERSION%%[![:space:]]*}"}"
VERSION="${VERSION%"${VERSION##*[![:space:]]}"}"
if [[ "$VERSION" != v* ]]; then
VERSION="v$VERSION"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Invalid release tag '$VERSION'. Use vX.Y.Z, for example v1.2.3."
exit 1
fi
if ! git ls-remote --exit-code --tags origin "refs/tags/$VERSION" >/dev/null 2>&1; then
echo "::error::Tag '$VERSION' does not exist on origin."
exit 1
fi
TAG_COMMIT="$(git rev-list -n 1 "$VERSION")"
MASTER_COMMIT="$(git rev-parse HEAD)"
echo "Overwrite release tag: $VERSION"
echo "Tag commit: $TAG_COMMIT"
echo "Master commit used for rebuilt assets: $MASTER_COMMIT"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == refs/heads/master ]] && ([[ "${{ github.event_name }}" == "workflow_dispatch" ]] || [[ "${{ github.event_name }}" == "schedule" ]]); then
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$LATEST_TAG" ]; then
@@ -505,7 +547,7 @@ jobs:
name: "🚀 Release Artifacts"
runs-on: ubuntu-latest
needs: [prepare, build-linux, build-windows-amd64, merge-manifest]
if: startsWith(github.ref, 'refs/tags/')
if: startsWith(github.ref, 'refs/tags/') || (github.event_name == 'workflow_dispatch' && inputs.overwrite_existing_release == true)
permissions:
contents: write
steps:
@@ -515,32 +557,42 @@ jobs:
fetch-depth: 0
- name: Prepare Release Notes Context
env:
RELEASE_VERSION: ${{ needs.prepare.outputs.version }}
OVERWRITE_EXISTING_RELEASE: ${{ inputs.overwrite_existing_release }}
run: |
set -euo pipefail
git fetch --tags --force origin
CURRENT_TAG="${GITHUB_REF_NAME}"
CURRENT_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${CURRENT_COMMIT}^" 2>/dev/null || true)"
CURRENT_TAG="$RELEASE_VERSION"
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ] && [ "${OVERWRITE_EXISTING_RELEASE}" = "true" ]; then
CURRENT_COMMIT="$(git rev-parse HEAD)"
TAG_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${TAG_COMMIT}^" 2>/dev/null || true)"
else
CURRENT_COMMIT="$(git rev-list -n 1 "$CURRENT_TAG")"
PREVIOUS_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "${CURRENT_COMMIT}^" 2>/dev/null || true)"
fi
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
if [ -n "$PREVIOUS_TAG" ]; then
RANGE_LABEL="$PREVIOUS_TAG..$CURRENT_TAG"
git log --pretty=format:'- %h %s' "$PREVIOUS_TAG..$CURRENT_TAG" > commits.md
git diff --name-only "$PREVIOUS_TAG" "$CURRENT_TAG" > changed-files.md
git diff --stat "$PREVIOUS_TAG" "$CURRENT_TAG" > diffstat.md
RANGE_LABEL="$PREVIOUS_TAG..$CURRENT_COMMIT"
git log --pretty=format:'- %h %s' "$PREVIOUS_TAG..$CURRENT_COMMIT" > commits.md
git diff --name-only "$PREVIOUS_TAG" "$CURRENT_COMMIT" > changed-files.md
git diff --stat "$PREVIOUS_TAG" "$CURRENT_COMMIT" > diffstat.md
else
RANGE_LABEL="$CURRENT_TAG"
git log --pretty=format:'- %h %s' "$CURRENT_TAG" > commits.md
git diff --name-only "$EMPTY_TREE" "$CURRENT_TAG^{tree}" > changed-files.md
git diff --stat "$EMPTY_TREE" "$CURRENT_TAG^{tree}" > diffstat.md
RANGE_LABEL="$CURRENT_COMMIT"
git log --pretty=format:'- %h %s' "$CURRENT_COMMIT" > commits.md
git diff --name-only "$EMPTY_TREE" "$CURRENT_COMMIT^{tree}" > changed-files.md
git diff --stat "$EMPTY_TREE" "$CURRENT_COMMIT^{tree}" > diffstat.md
fi
{
echo "# Release Context"
echo
echo "Current tag: $CURRENT_TAG"
echo "Current commit: $CURRENT_COMMIT"
echo "Previous tag: ${PREVIOUS_TAG:-none}"
echo "Range: $RANGE_LABEL"
echo
@@ -687,6 +739,14 @@ jobs:
- `SHA256SUMS`
EOF
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.overwrite_existing_release }}" = "true" ]; then
{
echo
echo "<!-- rebuilt-from: $(git rev-parse HEAD) -->"
echo "<!-- rebuilt-at: $(date -u +%Y-%m-%dT%H:%M:%SZ) -->"
} >> release-notes.md
fi
- name: Download all artifacts
uses: actions/download-artifact@v8
with:
@@ -707,9 +767,11 @@ jobs:
- name: Create Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
tag_name: ${{ needs.prepare.outputs.version }}
files: |
artifacts/linux-*/*.tar.gz
artifacts/openwrt-*/*.apk
artifacts/windows-*/*.zip
SHA256SUMS
body_path: release-notes.md
overwrite_files: true

View File

@@ -3,15 +3,23 @@ name: Sync Dev to Master
on:
workflow_dispatch:
inputs:
release_mode:
description: "Release mode: new creates a tag, overwrite rebuilds an existing release, sync_only only syncs branches."
required: true
default: "new"
type: choice
options:
- new
- overwrite
- sync_only
version:
description: "Release tag, e.g. v1.2.3. Leave empty to auto bump patch. Used only when create_release_tag is true."
description: "Release tag, e.g. v1.2.3. Empty auto bumps for new or uses latest existing tag for overwrite."
required: false
type: string
create_release_tag:
description: "Create and push a new release tag after syncing."
confirm_overwrite:
description: "Type OVERWRITE to confirm rebuilding an existing release."
required: false
default: true
type: boolean
type: string
concurrency:
group: sync-dev-to-master
@@ -84,10 +92,13 @@ jobs:
git push origin master
fi
- name: Create and Push Release Tag
if: ${{ inputs.create_release_tag != false }}
- name: Resolve Release Tag
id: release_tag
if: ${{ inputs.release_mode != 'sync_only' }}
env:
REQUESTED_VERSION: ${{ github.event.inputs.version }}
RELEASE_MODE: ${{ inputs.release_mode }}
REQUESTED_VERSION: ${{ inputs.version }}
CONFIRM_OVERWRITE: ${{ inputs.confirm_overwrite }}
run: |
set -euo pipefail
@@ -97,6 +108,11 @@ jobs:
REQUESTED_VERSION="${REQUESTED_VERSION#"${REQUESTED_VERSION%%[![:space:]]*}"}"
REQUESTED_VERSION="${REQUESTED_VERSION%"${REQUESTED_VERSION##*[![:space:]]}"}"
if [ "$RELEASE_MODE" = "overwrite" ] && [ "${CONFIRM_OVERWRITE:-}" != "OVERWRITE" ]; then
echo "::error::confirm_overwrite must be exactly 'OVERWRITE' when release_mode=overwrite."
exit 1
fi
if [ -n "$REQUESTED_VERSION" ]; then
TAG_NAME="$REQUESTED_VERSION"
if [[ "$TAG_NAME" != v* ]]; then
@@ -107,13 +123,23 @@ jobs:
LATEST_TAG="$(git tag --list 'v*.*.*' --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)"
if [ -z "$LATEST_TAG" ]; then
TAG_NAME="v0.1.0"
echo "No existing vX.Y.Z tag found. Starting at $TAG_NAME"
if [ "$RELEASE_MODE" = "new" ]; then
TAG_NAME="v0.1.0"
echo "No existing vX.Y.Z tag found. Starting at $TAG_NAME"
else
echo "::error::No existing vX.Y.Z tag found to overwrite."
exit 1
fi
else
VERSION="${LATEST_TAG#v}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
TAG_NAME="v${MAJOR}.${MINOR}.$((PATCH + 1))"
echo "Auto bumped release tag from $LATEST_TAG to $TAG_NAME"
if [ "$RELEASE_MODE" = "new" ]; then
VERSION="${LATEST_TAG#v}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
TAG_NAME="v${MAJOR}.${MINOR}.$((PATCH + 1))"
echo "Auto bumped release tag from $LATEST_TAG to $TAG_NAME"
else
TAG_NAME="$LATEST_TAG"
echo "No release tag requested. Using latest existing release tag: $TAG_NAME"
fi
fi
fi
@@ -122,21 +148,62 @@ jobs:
exit 1
fi
if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
echo "::error::Tag '$TAG_NAME' already exists locally."
exit 1
if [ "$RELEASE_MODE" = "new" ]; then
if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
echo "::error::Tag '$TAG_NAME' already exists locally."
exit 1
fi
if git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
echo "::error::Tag '$TAG_NAME' already exists on origin."
exit 1
fi
else
if ! git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
echo "::error::Tag '$TAG_NAME' does not exist locally."
exit 1
fi
if ! git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
echo "::error::Tag '$TAG_NAME' does not exist on origin."
exit 1
fi
TAG_COMMIT="$(git rev-list -n 1 "$TAG_NAME")"
MASTER_COMMIT="$(git rev-parse HEAD)"
echo "Overwrite release tag: $TAG_NAME"
echo "Tag commit: $TAG_COMMIT"
echo "Master commit used for rebuilt assets: $MASTER_COMMIT"
fi
if git ls-remote --exit-code --tags origin "refs/tags/$TAG_NAME" >/dev/null 2>&1; then
echo "::error::Tag '$TAG_NAME' already exists on origin."
exit 1
fi
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
- name: Create and Push Release Tag
if: ${{ inputs.release_mode == 'new' }}
env:
TAG_NAME: ${{ steps.release_tag.outputs.tag_name }}
run: |
set -euo pipefail
git tag -a "$TAG_NAME" -m "Release $TAG_NAME"
git push origin "$TAG_NAME"
- name: Skip Release Tag
if: ${{ inputs.create_release_tag == false }}
- name: Dispatch Overwrite Release Build
if: ${{ inputs.release_mode == 'overwrite' }}
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }}
TAG_NAME: ${{ steps.release_tag.outputs.tag_name }}
run: |
echo "create_release_tag=false; synced dev to master without creating a release tag."
set -euo pipefail
gh workflow run build-dockerhub.yml \
--ref master \
-f release_tag="$TAG_NAME" \
-f overwrite_existing_release=true
echo "Dispatched release rebuild for $TAG_NAME from master."
- name: Skip Release
if: ${{ inputs.release_mode == 'sync_only' }}
run: |
echo "release_mode=sync_only; synced dev to master without creating or rebuilding a release."

View File

@@ -720,7 +720,7 @@ std::string page(Request &, Response &response) {
<section class="section">
<div class="section-head">
<div>
<h2><span data-lang="en">Country Distribution</span><span data-lang="zh"></span></h2>
<h2><span data-lang="en">Country / Region Distribution</span><span data-lang="zh"></span></h2>
<div class="state-line" id="map-range-label">-</div>
</div>
<div class="section-actions">
@@ -741,7 +741,7 @@ std::string page(Request &, Response &response) {
<section class="section">
<div class="section-head">
<div>
<h2><span data-lang="en">Country Ranking</span><span data-lang="zh"></span></h2>
<h2><span data-lang="en">Country / Region Ranking</span><span data-lang="zh"></span></h2>
<div class="state-line" id="country-ranking-range">-</div>
</div>
<div class="section-actions">
@@ -834,6 +834,7 @@ std::string page(Request &, Response &response) {
var selectedRankingWindow = localStorage.getItem(RANKING_WINDOW_STORAGE_KEY) || "lifetime";
var refreshIntervalSeconds = Number(localStorage.getItem(REFRESH_INTERVAL_STORAGE_KEY) || 3);
var refreshTimer = null;
var hourlyChartInitialized = new WeakMap();
function isZh() { return /^zh\b/i.test(document.documentElement.lang); }
function text(en, zh) { return isZh() ? zh : en; }
@@ -975,25 +976,43 @@ std::string page(Request &, Response &response) {
animateCounters(metricsEl);
}
function renderHourlyChart(container, series, field, className, labelEn, labelZh) {
container.textContent = "";
if (!series.length) {
container.textContent = "";
hourlyChartInitialized.delete(container);
var empty = document.createElement("div");
empty.className = "empty compact";
empty.textContent = text("No hourly data", "暂无小时数据");
container.appendChild(empty);
return;
}
container.querySelectorAll(".empty").forEach(function (node) { node.remove(); });
var max = Math.max(1, ...series.map(function (item) { return item[field] || 0; }));
series.forEach(function (item) {
var seen = new Set();
var initialized = hourlyChartInitialized.get(container) === true;
series.forEach(function (item, index) {
var value = item[field] || 0;
var bar = document.createElement("div");
bar.className = "bar " + className;
bar.style.height = "4px";
var key = String(item.time || index);
seen.add(key);
var bar = container.querySelector('[data-hour-key="' + key + '"]');
if (!bar) {
bar = document.createElement("div");
bar.className = "bar " + className;
bar.setAttribute("data-hour-key", key);
bar.style.height = initialized ? Math.max(4, Math.round((value / max) * 132)) + "px" : "4px";
} else {
bar.className = "bar " + className;
}
bar.title = fmtTime(item.time) + " / " + text(labelEn, labelZh) + " " + number(value);
container.appendChild(bar);
var targetHeight = Math.max(4, Math.round((value / max) * 132)) + "px";
requestAnimationFrame(function () { bar.style.height = targetHeight; });
if (bar.style.height !== targetHeight)
requestAnimationFrame(function () { bar.style.height = targetHeight; });
});
container.querySelectorAll("[data-hour-key]").forEach(function (bar) {
if (!seen.has(bar.getAttribute("data-hour-key")))
bar.remove();
});
hourlyChartInitialized.set(container, true);
}
function renderHourlyCharts(series) {
series = series || [];
@@ -1078,47 +1097,63 @@ std::string page(Request &, Response &response) {
refreshTimer = setInterval(refresh, refreshIntervalSeconds * 1000);
}
function renderRankingPanel(container, countries, field, total, emptyMessageEn, emptyMessageZh) {
container.textContent = "";
var ranked = countries.filter(function (item) { return (item[field] || 0) > 0; })
.sort(function (a, b) { return (b[field] || 0) - (a[field] || 0); })
.slice(0, 12);
if (!ranked.length) {
container.textContent = "";
var empty = document.createElement("div");
empty.className = "empty compact";
empty.textContent = text(emptyMessageEn, emptyMessageZh);
container.appendChild(empty);
return;
}
container.querySelectorAll(".empty").forEach(function (node) { node.remove(); });
var max = Math.max(1, ...ranked.map(function (item) { return item[field] || 0; }));
var seen = new Set();
ranked.forEach(function (item) {
var value = item[field] || 0;
var row = document.createElement("div");
row.className = "ranking-row";
var key = field + ":" + item.code;
seen.add(key);
var row = container.querySelector('[data-rank-key="' + key + '"]');
var fill;
if (!row) {
row = document.createElement("div");
row.className = "ranking-row";
row.setAttribute("data-rank-key", key);
var country = document.createElement("div");
country.className = "rank-country";
country.innerHTML = '<span class="country-icon">' + countryIcon(item.code) + '</span><span class="code-badge">' + item.code + '</span><span class="rank-country-name">' + countryName(item.code) + '</span>';
var country = document.createElement("div");
country.className = "rank-country";
var metric = document.createElement("div");
metric.className = "rank-metric";
var values = document.createElement("div");
values.className = "rank-values";
values.innerHTML = '<span>' + countValue("rank:" + field + ":" + item.code, value) + '</span><span>' + percentage(value, total) + '</span>';
var track = document.createElement("div");
track.className = "rank-bar-track";
var fill = document.createElement("div");
fill.className = "rank-bar-fill" + (field === "rule_conversions" ? " rule" : "");
fill.style.setProperty("--rank-width", "0%");
track.appendChild(fill);
metric.appendChild(values);
metric.appendChild(track);
row.appendChild(country);
row.appendChild(metric);
var metric = document.createElement("div");
metric.className = "rank-metric";
var values = document.createElement("div");
values.className = "rank-values";
var track = document.createElement("div");
track.className = "rank-bar-track";
fill = document.createElement("div");
fill.className = "rank-bar-fill" + (field === "rule_conversions" ? " rule" : "");
fill.style.setProperty("--rank-width", "0%");
track.appendChild(fill);
metric.appendChild(values);
metric.appendChild(track);
row.appendChild(country);
row.appendChild(metric);
}
row.querySelector(".rank-country").innerHTML = '<span class="country-icon">' + countryIcon(item.code) + '</span><span class="code-badge">' + item.code + '</span><span class="rank-country-name">' + countryName(item.code) + '</span>';
row.querySelector(".rank-values").innerHTML = '<span>' + countValue("rank:" + field + ":" + item.code, value) + '</span><span>' + percentage(value, total) + '</span>';
fill = row.querySelector(".rank-bar-fill");
container.appendChild(row);
var targetWidth = Math.max(4, Math.round((value / max) * 100)) + "%";
requestAnimationFrame(function () {
fill.style.setProperty("--rank-width", Math.max(4, Math.round((value / max) * 100)) + "%");
if (fill.style.getPropertyValue("--rank-width") !== targetWidth)
fill.style.setProperty("--rank-width", targetWidth);
});
});
container.querySelectorAll("[data-rank-key]").forEach(function (row) {
if (!seen.has(row.getAttribute("data-rank-key")))
row.remove();
});
animateCounters(container);
}
function renderMapCountries(countries) {
@@ -1129,7 +1164,7 @@ std::string page(Request &, Response &response) {
updateRangeTabs(mapTabs, "data-map-window", selectedMapWindow);
var visibleCountries = countries.filter(function (item) { return item.code !== "ZZ" && item.code !== "XX"; });
mapRangeLabel.textContent = text("Showing ", "当前范围:") + label(countryConfig);
countryStatus.textContent = text("Countries ", "国家 ") + number(visibleCountries.length);
countryStatus.textContent = text("Countries / Regions ", "国家和地区 ") + number(visibleCountries.length);
}
function renderRankingCountries(countries) {
var countryConfig = windowConfig(selectedRankingWindow, RANGE_WINDOWS);