feat: Add reusable workflow to sync branches and tags - #158
Conversation
There was a problem hiding this comment.
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.ymlto 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.ymlto 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.
6fccd80 to
2655b3a
Compare
There was a problem hiding this comment.
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.shand 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
| # 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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Are you sure I need workflow scope?
There was a problem hiding this comment.
Yes, without it GitHub will refuse to sync any file in .github/workflows.
| # "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, |
There was a problem hiding this comment.
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)
| # (default: false) | ||
| # FORCE "true" to overwrite diverged destination branches. | ||
| # (default: false) | ||
| # WORKDIR Directory to initialize the working repo in. |
There was a problem hiding this comment.
I don't think this is needed - let's always use mktemp?
| @@ -0,0 +1,83 @@ | |||
| #!/usr/bin/env bash | |||
There was a problem hiding this comment.
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
| - `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`. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Done. I did not add the encrypt/decrypt token actions to the new Git section, but can move them there if you'd like.
| 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 |
There was a problem hiding this comment.
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
| - name: Checkout repository | ||
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | ||
|
|
||
| - name: Test clone-repo |
There was a problem hiding this comment.
| - name: Test clone-repo | |
| - name: Run clone-repo |
There was a problem hiding this comment.
Please, apply this suggestion everywhere where it makes sense
| repository: ${{ github.repository }} | ||
| token: ${{ github.token }} | ||
|
|
||
| - name: Confirm results |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Please, apply this suggestion everywhere where it makes sense
There was a problem hiding this comment.
Some comments here also apply to the sync-tags file
mathbunnyru
left a comment
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
| branches: | |
| branches_globs: |
There was a problem hiding this comment.
I renamed it to branch_globs and updated all references accordingly.
| JSON="$(printf '%s' "${BRANCHES}" | jq --raw-input --compact-output \ | ||
| 'split(",") | map(gsub("^\\s+|\\s+$";"")) | map(select(length > 0))')" |
There was a problem hiding this comment.
Are you sure there is no simpler way in jq? :)
There was a problem hiding this comment.
I had Claude try to rewrite it, which it did, but it didn't get much shorter. Any suggestions?
| # 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 |
There was a problem hiding this comment.
| # 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 |
There was a problem hiding this comment.
As above, done although with small modification.
| # Sync each branch in its own job so they can run in parallel. | ||
| sync-branch: |
There was a problem hiding this comment.
| # 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: |
| - `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. |
There was a problem hiding this comment.
This is an implementation detail, and it's not meant to be used from outside, so please don't add it here
| 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/**" |
There was a problem hiding this comment.
Ah, this is important - will it work in a PR done from a fork?
Same question about other pull_requests you add
There was a problem hiding this comment.
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?
This change ports the
sync-branches-tagsworkflow 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:
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.