Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/sync-fork.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Sync Fork with Upstream

on:
schedule:
- cron: '0 4 * * *' # daily at 04:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Dry run – merge locally but do not push'
type: boolean
default: false
protect_workflows:
description: 'Restore .github/workflows from fork after sync'
type: boolean
default: true
force_sync:
description: 'Force push even if the fork branch has diverged (overwrites fork history!)'
type: boolean
default: false

jobs:
sync:
name: Sync fork
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Detect upstream repository
id: upstream
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PARENT=$(gh api repos/${{ github.repository }} --jq '.parent.full_name // empty')
if [ -z "$PARENT" ]; then
echo "This repository is not a fork or has no parent repository. Aborting." >&2
exit 1
fi
DEFAULT_BRANCH=$(gh api repos/$PARENT --jq '.default_branch')
echo "repo=$PARENT" >> "$GITHUB_OUTPUT"
echo "branch=$DEFAULT_BRANCH" >> "$GITHUB_OUTPUT"

- name: Checkout fork
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Capture current HEAD
id: pre_sync
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

- name: Sync with upstream
id: sync
env:
OPT_DRY_RUN: ${{ inputs.dry_run || 'false' }}
OPT_PROTECT_WORKFLOWS: ${{ inputs.protect_workflows || 'true' }}
OPT_FORCE_SYNC: ${{ inputs.force_sync || 'false' }}
run: |
PRE_SHA="${{ steps.pre_sync.outputs.sha }}"
BRANCH="${{ steps.upstream.outputs.branch }}"

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git remote add upstream "https://github.com/${{ steps.upstream.outputs.repo }}.git"
git fetch upstream
git checkout "$BRANCH"

if [ "$OPT_FORCE_SYNC" = "true" ]; then
git reset --hard "upstream/$BRANCH"
else
git merge --no-edit "upstream/$BRANCH"
fi

# Optionally restore local .github/workflows
if [ "$OPT_PROTECT_WORKFLOWS" = "true" ]; then
git checkout "$PRE_SHA" -- .github/workflows
if ! git diff --cached --quiet; then
git commit -m "chore: restore local .github/workflows after upstream sync"
fi
fi

if [ "$OPT_DRY_RUN" = "true" ]; then
echo "Dry run enabled – skipping push."
elif [ "$OPT_FORCE_SYNC" = "true" ]; then
git push --force origin "$BRANCH"
else
git push origin "$BRANCH"
fi

- name: Write job summary
if: always()
run: |
echo "## Sync Fork with Upstream" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Field | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---|" >> "$GITHUB_STEP_SUMMARY"
echo "| **Fork** | \`${{ github.repository }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Upstream** | \`${{ steps.upstream.outputs.repo }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Branch** | \`${{ steps.upstream.outputs.branch }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Triggered by** | \`${{ github.event_name }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Dry run** | ${{ inputs.dry_run == true && '✅ Yes' || '❌ No' }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Protect workflows** | ${{ inputs.protect_workflows == false && '❌ No' || '✅ Yes' }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Force sync** | ${{ inputs.force_sync == true && '⚠️ Yes' || '❌ No' }} |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Run at** | $(date -u '+%Y-%m-%d %H:%M:%S UTC') |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Status** | ${{ steps.sync.outcome == 'success' && '✅ Success' || '❌ Failed' }} |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"

PRE_SHA="${{ steps.pre_sync.outputs.sha }}"
NEW_COMMITS=$(git log "$PRE_SHA"..HEAD --oneline 2>/dev/null)

if [ -n "$NEW_COMMITS" ]; then
echo "### New commits" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| SHA | Message |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---|" >> "$GITHUB_STEP_SUMMARY"
git log "$PRE_SHA"..HEAD --pretty=format:"%h|%s" | while IFS='|' read -r sha msg; do
REPO="${{ steps.upstream.outputs.repo }}"
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the job summary, commit links are built using the upstream repo (REPO="${{ steps.upstream.outputs.repo }}"), but the range PRE_SHA..HEAD can include fork-only commits (e.g., a merge commit and/or the "restore local .github/workflows" commit). Those SHAs won’t exist in the upstream repo, causing 404 links. Point links at the fork repository (e.g., ${{ github.repository }}) or dynamically select upstream vs fork per commit.

Suggested change
REPO="${{ steps.upstream.outputs.repo }}"
REPO="${{ github.repository }}"

Copilot uses AI. Check for mistakes.
echo "| [\`$sha\`](https://github.com/$REPO/commit/$sha) | $msg |" >> "$GITHUB_STEP_SUMMARY"
done
Comment on lines +118 to +121
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The summary table generation uses | as a delimiter (--pretty=format:"%h|%s" and IFS='|'). Since commit subjects can legally contain |, this can corrupt parsing and the Markdown table. Consider using a delimiter that can’t appear in %s (e.g., NUL via %x00 and read -d '') or avoid parsing by splitting only the first token and treating the rest as the message.

Suggested change
git log "$PRE_SHA"..HEAD --pretty=format:"%h|%s" | while IFS='|' read -r sha msg; do
REPO="${{ steps.upstream.outputs.repo }}"
echo "| [\`$sha\`](https://github.com/$REPO/commit/$sha) | $msg |" >> "$GITHUB_STEP_SUMMARY"
done
while IFS= read -r -d '' sha && IFS= read -r -d '' msg; do
REPO="${{ steps.upstream.outputs.repo }}"
escaped_msg="${msg//|/\\|}"
echo "| [\`$sha\`](https://github.com/$REPO/commit/$sha) | $escaped_msg |" >> "$GITHUB_STEP_SUMMARY"
done < <(git log "$PRE_SHA"..HEAD --pretty=format:'%h%x00%s%x00')

Copilot uses AI. Check for mistakes.
else
echo "_No new commits – fork was already up to date._" >> "$GITHUB_STEP_SUMMARY"
fi
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ default self-signed Cert.
> - This is a **manual process**
> - You must re-run the script about **every 90 days** to renew the cert OR Setup a cronjob or service etc...
> - The certificate will only validate for the **Tailnet hostname**, not the LAN IP
> - You must have HTTPS enabled in your tailnet admin settings
> - You must have HTTPS & MagicDNS enabled in your tailnet admin settings
> - You must be using an up to date tailscale version, this works on both routers and kvm thanks to [Admon](https://github.com/admonstrator/glinet-tailscale-updater): ```bash wget -q https://get.admon.me/tailscale -O update-tailscale.sh ; sh update-tailscale.sh ```

---
Expand All @@ -40,6 +40,7 @@ The scripts are:
### Routers
- Flint 3 (GL-BE9300)
- Slate 7 (GL-BE3600)
- Spitz AX (GL-X3000)
- Puli AX (GL-XE3000)
- Slate 7 Pro (GL-BE10000)
- Other GL.iNet routers using nginx for HTTPS should work
Expand Down
Loading