Skip to content

feat: Add reusable workflow to sync branches and tags - #158

Open
bthomee wants to merge 8 commits into
mainfrom
bthomee/sync
Open

feat: Add reusable workflow to sync branches and tags#158
bthomee wants to merge 8 commits into
mainfrom
bthomee/sync

Conversation

@bthomee

@bthomee bthomee commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

This change ports the sync-branches-tags workflow over to this repo, turning it into a reusable workflow. A test workflow is added to confirm that the dry-run, regular, and force behavior work as expected, using the https://github.com/XRPLF/actions-sync-test-src and https://github.com/XRPLF/actions-sync-test-dst repositories.

To enable testing, the original workflow had to be modified so it could access local scripts and actions when called from a different repo.

To keep the testing workflow as concise as possible, the following helper actions are introduced or updated:

  • clone-repo
  • configure-git
  • configure-github-app (updated to not configure git anymore; callers who need it can use the action above)
  • decrypt-token
  • encrypt-token

The latter two actions are necessary to pass the GitHub App token between different jobs via GPG encryption, as GitHub Actions will otherwise not let you pass a secret between jobs.

Copilot AI review requested due to automatic review settings July 23, 2026 16:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Ports a branch/tag synchronization workflow into this repository as a reusable workflow_call workflow, along with bash scripts to perform the git operations and a CI workflow intended to test dry-run, normal, and force-sync behaviors.

Changes:

  • Add reusable workflow .github/workflows/sync-branches-tags.yml to sync selected branches (matrix) and then tags.
  • Add bash scripts to sync branches/tags using authenticated remotes and safe force semantics (--force-with-lease).
  • Add a test workflow .github/workflows/test-sync-branches-tags.yml to exercise dry-run, regular sync, and force overwrite flows.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
.github/workflows/test-sync-branches-tags.yml Adds an end-to-end workflow to validate dry-run/normal/force sync behavior between two test repos.
.github/workflows/sync-branches-tags.yml Introduces a reusable workflow_call entrypoint that runs branch sync in a matrix and tag sync after branches complete.
.github/scripts/sync-tags.sh Implements tag synchronization logic including dry-run and force-with-lease overwrite.
.github/scripts/sync-common-functions.sh Adds shared helpers for authenticated remotes and working repo setup/validation.
.github/scripts/sync-branches.sh Implements branch synchronization logic including fast-forward, dry-run, and force-with-lease overwrite.
Comments suppressed due to low confidence (6)

.github/workflows/test-sync-branches-tags.yml:92

  • This workflow clones the same repo name multiple times across steps. Because the workspace persists between steps, a later gh repo clone ${REPO} will fail if ${REPO} already exists from an earlier step. Remove the directory (or clone into a unique temp dir) before cloning.
          gh repo clone ${REPO}
          cd ${REPO}

.github/workflows/test-sync-branches-tags.yml:120

  • This workflow clones the same repo name multiple times across steps. Because the workspace persists between steps, a later gh repo clone ${REPO} will fail if ${REPO} already exists from an earlier step. Remove the directory (or clone into a unique temp dir) before cloning.
          gh repo clone ${REPO}
          cd ${REPO}

.github/workflows/test-sync-branches-tags.yml:138

  • This workflow clones the same repo name multiple times across steps. Because the workspace persists between steps, a later gh repo clone ${REPO} will fail if ${REPO} already exists from an earlier step. Remove the directory (or clone into a unique temp dir) before cloning.
          gh repo clone ${REPO}
          cd ${REPO}

.github/workflows/test-sync-branches-tags.yml:163

  • This workflow clones the same repo name multiple times across steps. Because the workspace persists between steps, a later gh repo clone ${REPO} will fail if ${REPO} already exists from an earlier step. Remove the directory (or clone into a unique temp dir) before cloning.
          gh repo clone ${REPO}
          cd ${REPO}

.github/workflows/test-sync-branches-tags.yml:179

  • This workflow clones the same repo name multiple times across steps. Because the workspace persists between steps, a later gh repo clone ${REPO} will fail if ${REPO} already exists from an earlier step. Remove the directory (or clone into a unique temp dir) before cloning.
          gh repo clone ${REPO}
          cd ${REPO}

.github/workflows/test-sync-branches-tags.yml:192

  • This workflow clones the same repo name multiple times across steps. Because the workspace persists between steps, a later gh repo clone ${REPO} will fail if ${REPO} already exists from an earlier step. Remove the directory (or clone into a unique temp dir) before cloning.
          gh repo clone ${REPO}
          cd ${REPO}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/workflows/test-sync-branches-tags.yml Outdated
Comment thread .github/scripts/sync-common-functions.sh Outdated
Comment thread .github/workflows/sync-branches-tags.yml
@bthomee
bthomee force-pushed the bthomee/sync branch 2 times, most recently from 6fccd80 to 2655b3a Compare July 24, 2026 00:18
@bthomee
bthomee requested a review from Copilot July 24, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

.github/workflows/sync-branches-tags.yml:104

  • Same issue as the branch job: this job checks out the caller repository but then runs .github/scripts/sync-tags.sh and uses ./decrypt-token, which won’t exist when this workflow is invoked from another repository.
      - name: Checkout repository
        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

Comment thread .github/workflows/sync-branches-tags.yml Outdated
Comment thread decrypt-token/action.yml Outdated
Comment thread decrypt-token/action.yml Outdated
@bthomee
bthomee requested a review from mathbunnyru July 24, 2026 15:20
Comment thread .github/scripts/sync-branches.sh Outdated
Comment on lines +5 to +6
# are skipped and cause a non-zero exit. Force mode overwrites diverged branches with a force-push
# (using --force-with-lease). Dry-run mode reports what would change without pushing anything.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would suggest removing force mode. If something like this happens, it's unlikely that we'll just force push and forget about it, it will probably require much more investigation of what happened.

We can add it back later if we need it, but only after very careful consideration, and I don't think we'll need it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can remove it from the workflow. I want to keep it in the scripts, since if there is a need I don't want us to have to run commands manually and possibly make mistakes.

# GH_TOKEN Token with read access to the source repository and write access to the
# destination. A public repository ignores the token as long as it is valid. When
# running this script locally, your personal access token must have at least the
# "repo", "read:org" (for branches in organization repos), and "workflow" scopes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are you sure I need workflow scope?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, without it GitHub will refuse to sync any file in .github/workflows.

Comment thread .github/scripts/sync-branches.sh Outdated
# "repo", "read:org" (for branches in organization repos), and "workflow" scopes.
# SOURCE_REPO Source repository, as "owner/repo" (github.com is assumed).
# DEST_REPO Destination repository, as "owner/repo" (github.com is assumed).
# BRANCH A single branch name or glob to synchronize. A glob may match several branches,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe let's call it BRANCHES_GLOB?
This way you can simplify descriptions too (because even a single branch name is a glob which matches only this single branch)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread .github/scripts/sync-branches.sh Outdated
# (default: false)
# FORCE "true" to overwrite diverged destination branches.
# (default: false)
# WORKDIR Directory to initialize the working repo in.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think this is needed - let's always use mktemp?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -0,0 +1,83 @@
#!/usr/bin/env bash

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If it's intended to be sources, I don't think you should say how to execute this file. And you should make it non-executable if you made it executable

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread README.md Outdated
Comment on lines +8 to +10
- `clone-repo`: Clones a repository authenticated with a token, so that subsequent plain git commands work without additional setup.
- `configure-git`: Configures the git user identity (name and email) for the current job.
- `configure-github-app`: Creates a token for a GitHub App and exposes its git user identity, for use with `configure-git`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How about we create a separate subsection, for newly added git-related operations?
I think they're quite closely related, and it would be great to have them documented nearby.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I did not add the encrypt/decrypt token actions to the new Git section, but can move them there if you'd like.

Comment thread .github/scripts/sync-branches.sh Outdated
Comment on lines +72 to +79
if ! git fetch --quiet "${SOURCE_REMOTE}" "+${BRANCH_REF}/${BRANCH}:${SOURCE_REF}/${BRANCH}"; then
local ls_status=0
git ls-remote --exit-code --heads "${SOURCE_REMOTE}" "${BRANCH_REF}/${BRANCH}" >/dev/null 2>&1 || ls_status=$?
if [ "${ls_status}" -eq 2 ]; then
die "Source branch(es) '${BRANCH}' not found in '${SOURCE_REPO}'." 2
fi
die "Failed to fetch source branch(es) '${BRANCH}' from '${SOURCE_REPO}'." 2
fi

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this script would be much easier, if we ran it with set -e, and would avoid too extensive error handling in the first place - git gives enough error messages, if it fails.

And there will be no need for very long comments explaining why it was done this way

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread .github/workflows/test-clone-repo.yml Outdated
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Test clone-repo

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- name: Test clone-repo
- name: Run clone-repo

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please, apply this suggestion everywhere where it makes sense

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread .github/workflows/test-clone-repo.yml Outdated
repository: ${{ github.repository }}
token: ${{ github.token }}

- name: Confirm results

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would also highly suggest making most checks as separate steps
The check can be rewritten as test commands.

It will be roughly the same amount of lines, but if some check fails, it will be really evident which assumption actually broke in the UI, even without checking logs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please, apply this suggestion everywhere where it makes sense

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some comments here also apply to the sync-tags file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

mathbunnyru
mathbunnyru previously approved these changes Jul 28, 2026

@mathbunnyru mathbunnyru left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall, LGTM, some naming can be done slightly better - I haven't marked all places, but it's nit

description: "Destination repository to synchronize to (e.g. owner/dst)."
type: string
required: true
branches:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
branches:
branches_globs:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I renamed it to branch_globs and updated all references accordingly.

Comment on lines +65 to +66
JSON="$(printf '%s' "${BRANCHES}" | jq --raw-input --compact-output \
'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))')"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are you sure there is no simpler way in jq? :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had Claude try to rewrite it, which it did, but it didn't get much shorter. Any suggestions?

Comment on lines +48 to +51
# Determine the list of branches to synchronize into a JSON array that the matrix below expands,
# and the owner and repository names the GitHub App token must be scoped to.
get-branches:
name: Get branches

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Determine the list of branches to synchronize into a JSON array that the matrix below expands,
# and the owner and repository names the GitHub App token must be scoped to.
get-branches:
name: Get branches
# Determine the list of branches globs to synchronize into a JSON array that the matrix below expands,
# and the owner and repository names the GitHub App token must be scoped to.
get-branches-globs:
name: Get branches globs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As above, done although with small modification.

Comment on lines +92 to +93
# Sync each branch in its own job so they can run in parallel.
sync-branch:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Sync each branch in its own job so they can run in parallel.
sync-branch:
# Sync each branch glob in its own job so they can run in parallel.
sync-branch-glob:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment thread README.md Outdated
- `determine-tidy-files.yml`: Determines which files have been modified in a Pull Request and sets an output variables with the list of those files.
- `pre-commit.yml`: Runs `pre-commit` checks on code changes.
- `pre-commit-autoupdate.yml`: Runs `pre-commit autoupdate` to update pre-commit hooks.
- `reusable-build-docker-image.yml`: Builds a single-platform Docker image and pushes it to a container registry.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is an implementation detail, and it's not meant to be used from outside, so please don't add it here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed.

Comment on lines +4 to +11
pull_request:
paths:
- ".github/scripts/sync-*"
- ".github/workflows/sync-branches-tags.yml"
- ".github/workflows/test-sync-branches-tags.yml"
- "clone-repo/**"
- "configure-git/**"
- "configure-github-app/**"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, this is important - will it work in a PR done from a fork?
Same question about other pull_requests you add

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It depends on what you're trying to accomplish. The workflow currently only accepts GitHub App credentials, for which it then generates a GitHub token; the owner is required to be the same as a GitHub App is installed in an org, so you can only sync repos within the same org. My understanding is that even if the GitHub App is public and you can install it in multiple orgs, then you'd still end up with a different client ID and private key, so you'd still not be able to sync across orgs.

If this reusable workflow is supposed to be callable from a fork, then I can add an additional gh_token as input - if provided it will use that one instead. Personal access tokens can be used to sync across orgs - the workflow will be able to sync whatever the account has access to. Note that the script that the workflow calls only uses the GitHub token and that works for both PATs and GitHub Apps (since the token is fetched on demand for the GitHub App).

If you mean whether the test sync branches and tags workflow will work (or at least no nothing instead of failing) when someone clones the repo, then I'm pretty sure the answer is "no" since it's unlikely someone else will have (i) also cloned the same source and destination test repos, and (ii) set up their own GitHub App and added the client ID and private key as secrets. Would adding if: ${{ github.repository == 'XRPLF/actions' }} to each job be sufficient?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants