ci(upstream): track fork drift with check script and weekly workflow

Add check-upstream.sh, generated status artifacts, merge guide, and a
scheduled Forgejo job to refresh upstream sync visibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
littlecodedragon 2026-07-10 10:12:13 +02:00
parent 49a0644f68
commit d69ce1c388
6 changed files with 466 additions and 0 deletions

View file

@ -0,0 +1,100 @@
name: Upstream sync status
# Lightweight job: fetch upstream from GitHub, write status files, optional issue.
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: write
issues: write
concurrency:
group: upstream-status
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Checkout and check upstream
shell: bash
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BEHIND_WARN: '10'
run: |
set -euo pipefail
rm -rf .git
git init .
origin_url="${GITHUB_SERVER_URL#https://}"
origin_url="${origin_url#http://}"
git remote add origin "https://x-access-token:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git"
git -c http.sslVerify=false fetch --prune origin "${GITHUB_REF_NAME}"
git checkout --force FETCH_HEAD
git remote add upstream https://github.com/komikku-app/komikku.git
chmod +x scripts/check-upstream.sh
./scripts/check-upstream.sh --write
behind="$(python3 -c 'import json; print(json.load(open("docs/upstream-status.json"))["behind"])')"
ahead="$(python3 -c 'import json; print(json.load(open("docs/upstream-status.json"))["ahead"])')"
status="$(python3 -c 'import json; print(json.load(open("docs/upstream-status.json"))["status"])')"
echo "BEHIND=$behind" >> "$GITHUB_ENV"
echo "AHEAD=$ahead" >> "$GITHUB_ENV"
echo "STATUS=$status" >> "$GITHUB_ENV"
- name: Commit status files
shell: bash
env:
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
git config user.name "forgejo-actions[bot]"
git config user.email "forgejo-actions[bot]@users.noreply.local"
git add UPSTREAM_STATUS.md docs/upstream-status.json
if git diff --staged --quiet; then
echo "No upstream status changes."
exit 0
fi
git commit -m "chore(ci): update upstream sync status"
origin_url="${GITHUB_SERVER_URL#https://}"
origin_url="${origin_url#http://}"
git push "https://x-access-token:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
- name: Open issue when far behind
if: env.STATUS == 'behind'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
title="Upstream sync: ${BEHIND} commits behind komikku-app/komikku"
body=$(cat <<EOF
The weekly upstream check found this fork is **${BEHIND} commits behind** upstream and **${AHEAD} commits ahead** with fork-specific changes.
See [UPSTREAM_STATUS.md](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${GITHUB_REF_NAME}/UPSTREAM_STATUS.md) and [docs/upstream-sync.md](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${GITHUB_REF_NAME}/docs/upstream-sync.md).
To merge upstream locally:
\`\`\`sh
git fetch upstream
git merge upstream/master
\`\`\`
EOF
)
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues?state=open&labels=upstream-sync"
existing="$(curl -fsSL -H "Authorization: token ${GH_TOKEN}" "$api" | python3 -c 'import json,sys; issues=json.load(sys.stdin); print(next((str(i["number"]) for i in issues if i.get("title")==sys.argv[1]), ""))' "$title")"
if [[ -n "$existing" ]]; then
curl -fsSL -X PATCH \
-H "Authorization: token ${GH_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c 'import json,sys; print(json.dumps({"body": sys.argv[1]}))' "$body")" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${existing}"
echo "Updated issue #${existing}"
exit 0
fi
curl -fsSL -X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c 'import json,sys; print(json.dumps({"title": sys.argv[1], "body": sys.argv[2], "labels": ["upstream-sync"]}))' "$title" "$body")" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues"
echo "Created upstream sync issue"

View file

@ -24,6 +24,10 @@
*Requires Android 8.0 or higher.* *Requires Android 8.0 or higher.*
Self-hosted Forgejo releases (including [Obtainium](https://github.com/ImranR98/Obtainium) install) are documented in [docs/forgejo-release-runner.md](docs/forgejo-release-runner.md).
**Fork upstream status:** see [UPSTREAM_STATUS.md](UPSTREAM_STATUS.md) (updated weekly; manual check: `./scripts/check-upstream.sh`). Merge guide: [docs/upstream-sync.md](docs/upstream-sync.md).
[![Sponsor me on GitHub](https://custom-icon-badges.demolab.com/badge/-Sponsor-ea4aaa?style=for-the-badge&logo=heart&logoColor=white)](https://github.com/sponsors/cuong-tran "Sponsor me on GitHub") [![Sponsor me on GitHub](https://custom-icon-badges.demolab.com/badge/-Sponsor-ea4aaa?style=for-the-badge&logo=heart&logoColor=white)](https://github.com/sponsors/cuong-tran "Sponsor me on GitHub")
<div align="left"> <div align="left">

48
UPSTREAM_STATUS.md Normal file
View file

@ -0,0 +1,48 @@
# Upstream sync status
> Auto-generated by [`scripts/check-upstream.sh`](scripts/check-upstream.sh). Last check: **2026-07-10 07:56 UTC**.
| | |
|---|---|
| Status | 🔴 **Behind upstream** |
| Behind upstream | **73** commits on [`upstream/master`](https://github.com/komikku-app/komikku.git) |
| Ahead of upstream | **66** fork-only commits on `main` |
| Merge base | `bdf81596e9` |
| Fork HEAD | `b299a9ce81` |
| Upstream HEAD | `87a173929b` |
## Action recommended
This fork is **73 commits behind** [komikku-app/komikku](https://github.com/komikku-app/komikku.git). Consider merging upstream soon.
See [docs/upstream-sync.md](docs/upstream-sync.md) for the manual sync procedure.
## Recent upstream commits not in fork
```
87a173929b Adapt network implementation for TachiyomiX 1.6 (mihonapp/mihon#3248)
4ae732a17b fix(deps): Matching coroutines version
eb84ab8439 Ensure old extension store is removed if index url changes (mihonapp/mihon#3408)
5e809fefd4 Add auto migration support from legacy extension store index (mihonapp/mihon#3398)
1763d947df chore(sync): migrate sync preference keys (#1747)
ce6a0ea7fa fix(extension-store): Migrate to extension store during restore/sync legacy backup (#1746)
da57b3db07 Change the term "Obsolete" to "Orphaned" for extensions (mihonapp/mihon#3383)
da7ee11bb3 Change extension repo to extension store and add support for newer extension index format (#1744)
21c1b85796 Add informative error for unapproved MAL titles (mihonapp/mihon#3155)
0f556ea9b5 Address bundleOf deprecation (mihonapp/mihon#3073)
```
## Recent fork-only commits
```
b299a9ce81 docs(reading-eta): document unified limits, long chapters, and settings
f178a79195 feat(reading-eta): unify library and reader limits; make caps tunable
e0f4262407 ci(forgejo): verify signed APKs with unzip + aapt before upload
6316b85a09 fix(release): tolerate Forgejo null body/assets in release JSON
f91af6fc72 ci(forgejo): shorten release notes to since-last-tag + cap
c15d280cb4 feat(eta): cross-manga page-rate priors for reading time left
07019175d1 fix(eta): tame reading-time estimates (idle + cumulative history)
9ce3f0ba29 ci(forgejo): group release notes by prefix and fork vs merges
7fab10c8ef CI: build release JSON with jq --rawfile (avoid ARG_MAX on large bodies
e89f342881 CI: fix release step env — multiline COMMIT_LOGS broke act_runner
```

16
docs/upstream-status.json Normal file
View file

@ -0,0 +1,16 @@
{
"schemaVersion": 1,
"checked_at": "2026-07-10 07:56 UTC",
"status": "behind",
"status_label": "Behind upstream",
"behind": 73,
"ahead": 66,
"fork_branch": "main",
"upstream_remote": "upstream",
"upstream_branch": "master",
"upstream_url": "https://github.com/komikku-app/komikku.git",
"merge_base": "bdf81596e9040eee0c3a4c32d81a05e13049ec71",
"fork_head": "b299a9ce8163d1692526d72413c81dd7fd38ab47",
"upstream_head": "87a173929bd3355e4ad2b66b6025de49c9feeaf5",
"behind_warn": 10
}

102
docs/upstream-sync.md Normal file
View file

@ -0,0 +1,102 @@
# Syncing with upstream (komikku-app/komikku)
This Forgejo fork tracks [komikku-app/komikku](https://github.com/komikku-app/komikku) on GitHub.
| Remote | URL | Branch |
|--------|-----|--------|
| `origin` | Your Forgejo instance | `main` |
| `upstream` | `https://github.com/komikku-app/komikku.git` | `master` |
Current divergence is recorded in [UPSTREAM_STATUS.md](../UPSTREAM_STATUS.md) (updated weekly by Forgejo Actions).
## Quick check
```sh
./scripts/check-upstream.sh
```
Write refreshed status files:
```sh
./scripts/check-upstream.sh --write
```
JSON only:
```sh
./scripts/check-upstream.sh --json
```
## One-time setup
If `upstream` is missing:
```sh
git remote add upstream https://github.com/komikku-app/komikku.git
git fetch upstream
```
## Merge upstream into the fork
1. Commit or stash local work.
2. Update refs and inspect divergence:
```sh
git fetch upstream
git fetch origin
./scripts/check-upstream.sh
git log --oneline main..upstream/master # incoming upstream commits
git log --oneline upstream/master..main # fork-only commits
```
3. Merge (recommended for shared `main`):
```sh
git checkout main
git merge upstream/master
```
Resolve conflicts, then:
```sh
git add -A
git commit # if merge was not auto-committed
```
4. Push to Forgejo (never force-push `main` unless you intend to):
```sh
git push origin main
```
## Expected conflict areas
Merges often conflict where the fork diverged from upstream:
- `.github/workflows/*.yml` — upstream GitHub Actions vs fork Forgejo release workflow
- `README.md` — fork-specific Forgejo / Obtainium notes
- `i18n-kmk/.../de/strings.xml` — parallel translation edits
- `data/src/main/sqldelight/tachiyomi/migrations/46.sqm` — schema migration numbering
Keep fork-specific CI in `.forgejo/workflows/` and upstream CI in `.github/workflows/`; resolve workflow conflicts by keeping both sides where possible.
## Rebase alternative
For a linear history on a private branch (not `main`):
```sh
git checkout -b sync/upstream-$(date +%Y%m%d)
git rebase upstream/master
```
Then test, merge the sync branch into `main`, and push.
## Automation
Workflow: [`.forgejo/workflows/upstream_status.yml`](../.forgejo/workflows/upstream_status.yml)
- Runs every Monday 06:00 UTC and on manual dispatch
- Updates `UPSTREAM_STATUS.md` and `docs/upstream-status.json`
- Opens or updates a single `upstream-sync` issue when more than 10 commits behind
On a private Forgejo host, dynamic shields.io badges cannot read your repo; use `UPSTREAM_STATUS.md` or the JSON file instead.

196
scripts/check-upstream.sh Executable file
View file

@ -0,0 +1,196 @@
#!/usr/bin/env bash
# Compare this fork against upstream and optionally write UPSTREAM_STATUS.md.
set -euo pipefail
UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}"
UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/komikku-app/komikku.git}"
UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-master}"
FORK_BRANCH="${FORK_BRANCH:-main}"
BEHIND_WARN="${BEHIND_WARN:-10}"
OUTPUT_MD="${OUTPUT_MD:-UPSTREAM_STATUS.md}"
OUTPUT_JSON="${OUTPUT_JSON:-docs/upstream-status.json}"
FETCH="${FETCH:-1}"
WRITE="${WRITE:-0}"
usage() {
cat <<'EOF'
Usage: scripts/check-upstream.sh [options]
Options:
--no-fetch Skip git fetch upstream
--write Write UPSTREAM_STATUS.md and docs/upstream-status.json
--json Print JSON status to stdout
-h, --help Show this help
Environment:
UPSTREAM_REMOTE Git remote name (default: upstream)
UPSTREAM_BRANCH Upstream branch (default: master)
FORK_BRANCH Local fork branch (default: main)
BEHIND_WARN Warn when behind by this many commits (default: 10)
EOF
}
print_json=0
while [[ $# -gt 0 ]]; do
case "$1" in
--no-fetch) FETCH=0 ;;
--write) WRITE=1 ;;
--json) print_json=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
shift
done
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
if ! git rev-parse --verify "$FORK_BRANCH" >/dev/null 2>&1; then
echo "Fork branch not found: $FORK_BRANCH" >&2
exit 1
fi
if ! git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1; then
echo "Adding upstream remote: $UPSTREAM_REMOTE -> $UPSTREAM_URL" >&2
git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
fi
if [[ "$FETCH" == "1" ]]; then
git fetch "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" --prune
fi
upstream_ref="$UPSTREAM_REMOTE/$UPSTREAM_BRANCH"
if ! git rev-parse --verify "$upstream_ref" >/dev/null 2>&1; then
echo "Upstream ref not found: $upstream_ref" >&2
exit 1
fi
merge_base="$(git merge-base "$FORK_BRANCH" "$upstream_ref")"
behind="$(git rev-list --count "$FORK_BRANCH".."$upstream_ref")"
ahead="$(git rev-list --count "$upstream_ref".."$FORK_BRANCH")"
fork_head="$(git rev-parse --short "$FORK_BRANCH")"
upstream_head="$(git rev-parse --short "$upstream_ref")"
checked_at="$(date -u +"%Y-%m-%d %H:%M UTC")"
if [[ "$behind" -eq 0 ]]; then
status="up_to_date"
status_label="Up to date"
status_emoji="✅"
elif [[ "$behind" -le "$BEHIND_WARN" ]]; then
status="slightly_behind"
status_label="Slightly behind"
status_emoji="🟡"
else
status="behind"
status_label="Behind upstream"
status_emoji="🔴"
fi
upstream_sample="$(git log --oneline "$FORK_BRANCH".."$upstream_ref" -10)"
fork_sample="$(git log --oneline "$upstream_ref".."$FORK_BRANCH" -10)"
if [[ "$print_json" == "1" ]]; then
python3 - <<PY
import json
print(json.dumps({
"checked_at": "$checked_at",
"status": "$status",
"status_label": "$status_label",
"behind": $behind,
"ahead": $ahead,
"fork_branch": "$FORK_BRANCH",
"upstream_remote": "$UPSTREAM_REMOTE",
"upstream_branch": "$UPSTREAM_BRANCH",
"upstream_url": "$UPSTREAM_URL",
"merge_base": "$(git rev-parse --short "$merge_base")",
"fork_head": "$fork_head",
"upstream_head": "$upstream_head",
"behind_warn": $BEHIND_WARN,
}, indent=2))
PY
fi
echo "Upstream: $UPSTREAM_REMOTE ($UPSTREAM_URL) @ $UPSTREAM_BRANCH"
echo "Fork: $FORK_BRANCH @ $fork_head"
echo "Merge base: $(git rev-parse --short "$merge_base")"
echo "Behind upstream: $behind commits"
echo "Ahead of upstream: $ahead commits"
echo "Status: $status_emoji $status_label"
if [[ "$WRITE" != "1" ]]; then
exit 0
fi
mkdir -p "$(dirname "$OUTPUT_JSON")"
python3 - <<PY
import json
from pathlib import Path
Path("$(dirname "$OUTPUT_JSON")").mkdir(parents=True, exist_ok=True)
json.dump({
"schemaVersion": 1,
"checked_at": "$checked_at",
"status": "$status",
"status_label": "$status_label",
"behind": $behind,
"ahead": $ahead,
"fork_branch": "$FORK_BRANCH",
"upstream_remote": "$UPSTREAM_REMOTE",
"upstream_branch": "$UPSTREAM_BRANCH",
"upstream_url": "$UPSTREAM_URL",
"merge_base": "$(git rev-parse "$merge_base")",
"fork_head": "$(git rev-parse "$FORK_BRANCH")",
"upstream_head": "$(git rev-parse "$upstream_ref")",
"behind_warn": $BEHIND_WARN,
}, open("$OUTPUT_JSON", "w"), indent=2)
PY
{
cat <<EOF
# Upstream sync status
> Auto-generated by [\`scripts/check-upstream.sh\`](scripts/check-upstream.sh). Last check: **$checked_at**.
| | |
|---|---|
| Status | $status_emoji **$status_label** |
| Behind upstream | **$behind** commits on [\`$UPSTREAM_REMOTE/$UPSTREAM_BRANCH\`]($UPSTREAM_URL) |
| Ahead of upstream | **$ahead** fork-only commits on \`$FORK_BRANCH\` |
| Merge base | \`$(git rev-parse --short "$merge_base")\` |
| Fork HEAD | \`$fork_head\` |
| Upstream HEAD | \`$upstream_head\` |
EOF
if [[ "$behind" -gt 0 ]]; then
cat <<EOF
## Action recommended
This fork is **$behind commits behind** [komikku-app/komikku]($UPSTREAM_URL). Consider merging upstream soon.
See [docs/upstream-sync.md](docs/upstream-sync.md) for the manual sync procedure.
EOF
else
echo "## No merge needed"
echo
echo "The fork includes all commits from upstream \`$UPSTREAM_BRANCH\`."
echo
fi
cat <<EOF
## Recent upstream commits not in fork
\`\`\`
$(printf '%s\n' "$upstream_sample")
\`\`\`
## Recent fork-only commits
\`\`\`
$(printf '%s\n' "$fork_sample")
\`\`\`
EOF
} >"$OUTPUT_MD"
echo "Wrote $OUTPUT_MD and $OUTPUT_JSON"