Skip to content

Divinci-AI/sync-fork-action

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sync Fork Action

Keep a branch on your fork automatically in sync with an upstream repository.

A small, fully auditable GitHub Action: it is a composite action whose entire behaviour lives in one readable scripts/sync.sh — no Node runtime, no compiled dist/, no vendored node_modules/ to trust.

- uses: Divinci-AI/sync-fork-action@v1
  with:
    upstream_repository: octocat/Hello-World
    upstream_branch: master
    target_branch: main

Why another fork-sync action?

Most existing fork-sync actions are Node actions that ship a bundled dist/index.js or a node_modules/ tree — you can't easily see what runs in your repo with contents: write. This one is deliberately boring:

  • One shell script you can read in two minutes. The composite action just sets environment variables and runs scripts/sync.sh.
  • Inputs are passed via env:, never interpolated into the script body, so an input value can't be parsed as shell (script-injection safe).
  • Strict input validation on the repository slug and branch names blocks argument/option injection into git (e.g. a branch named --upload-pack=…).
  • Tokens are sent via http.extraheader, never embedded in a remote URL, so they can't leak through logs or git remote -v.
  • No global state. Git identity is set with repo-local config; the runner's global ~/.gitconfig is never touched.

Usage

Add a workflow to your fork (not the upstream). A ready-to-copy file is in examples/scheduled-sync.yml.

name: Sync fork with upstream
on:
  schedule:
    - cron: '0 * * * *'   # hourly
  workflow_dispatch:

permissions:
  contents: write          # least privilege: only needs to push to your fork

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # full history is required for merge/rebase
      - uses: Divinci-AI/sync-fork-action@v1
        with:
          upstream_repository: octocat/Hello-World
          upstream_branch: master
          target_branch: main

Run actions/checkout first with fetch-depth: 0. This action operates on the repository in the workspace and needs full history to integrate commits.

Inputs

Input Required Default Description
upstream_repository Upstream to sync from: owner/repo (resolved to https://github.com/owner/repo.git) or a full https:// URL.
upstream_branch Branch on the upstream to sync from (e.g. main).
target_branch Branch on your fork to sync into and push (e.g. main).
github_token ${{ github.token }} Token used to push to your fork (and to read the upstream on github.com when upstream_token is unset).
upstream_token '' Token used only to fetch a private upstream. Leave empty for public upstreams.
sync_mode merge merge, rebase, or reset — see below.
fetch_args '' Extra args appended to git fetch upstream (e.g. --tags). Trusted input.
merge_args '' Extra args for the integration command in merge/rebase mode (e.g. --ff-only). Trusted input.
push_args '' Extra args appended to git push. Trusted input.
git_user_name github-actions[bot] Author/committer name for the sync commit.
git_user_email …github-actions[bot]@users.noreply.github.com Author/committer email.
dry_run false When true, report what would sync but don't integrate or push.

Outputs

Output Description
has_new_commits true when the upstream had commits your target branch was missing.
commit_count Number of commits the target branch was behind upstream.
upstream_sha Resolved commit SHA of the upstream branch tip.
      - uses: Divinci-AI/sync-fork-action@v1
        id: sync
        with: { upstream_repository: octocat/Hello-World, upstream_branch: master, target_branch: main }
      - if: steps.sync.outputs.has_new_commits == 'true'
        run: echo "Synced ${{ steps.sync.outputs.commit_count }} commits"

Sync modes

Mode What it does When to use
merge (default) Merges upstream/<branch> into your target, creating a merge commit when histories diverge. You keep your own commits on the fork and want upstream changes merged in.
rebase Replays your target's commits on top of upstream, then force-pushes (--force-with-lease). You keep a small, linear set of changes on top of upstream.
reset Hard-resets the target to exactly match upstream, then force-pushes (--force-with-lease). An "untouched" mirror fork — discard any divergence and track upstream verbatim.

Both rebase and reset rewrite the target branch's history and therefore force-push (lease-guarded). Only use them on a branch you keep for upstream tracking, not one others push to. merge never force-pushes.

Recipe: auto-deploy a customer's Cloudflare Worker from a fork

A common setup: you control a source repo, a customer forks it, and you want every push you make to redeploy their Cloudflare Worker with no manual step. The fork syncs itself (triggered instantly, or on a schedule) and Cloudflare Workers Builds deploys on the resulting push.

See docs/cloudflare-fork-deploy.md for the full two-sided setup, including the .github/workflows/ push caveat, and docs/github-pat-setup.md for the click-by-click guide to creating the GitHub tokens with the right (least-privilege) scopes. Ready-to-copy workflows are in examples/origin-repo and examples/customer-fork.

Private upstreams

For a private upstream that the default github.token can't read, pass a token with read access as upstream_token. It is used only for the upstream fetch; pushes to your fork still use github_token.

      - uses: Divinci-AI/sync-fork-action@v1
        with:
          upstream_repository: my-org/private-upstream
          upstream_branch: main
          target_branch: main
          upstream_token: ${{ secrets.UPSTREAM_READ_TOKEN }}

Security notes

  • The *_args inputs are passed to git unquoted by design, so you can pass multiple flags. Treat them as trusted workflow configuration — never build them from untrusted data (issue/PR titles, comment bodies, etc.).
  • Everything else WE put on a git command line (repository, branches) is validated against a strict allowlist and rejected if it could be read as a flag.
  • Grant the workflow only permissions: contents: write.

This action was written from scratch, inspired by the idea behind earlier fork-sync actions but sharing none of their code, specifically so the entire trust surface is a single short shell script you can review yourself.

Development

The whole action is scripts/sync.sh. Tests are plain bash with no dependencies — they run the real script against throwaway local git repos.

bash tests/run.sh          # unit (validation/injection) + e2e (merge/rebase/reset/dry-run)
shellcheck --enable=all --severity=style scripts/sync.sh
actionlint

CI runs all three on every push and PR.

License

MIT © Divinci AI

About

Keep a fork branch in sync with an upstream repo. A small, fully-auditable composite GitHub Action — one shell script, no vendored code.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages