├── .Rbuildignore
├── .github
└── workflows
│ ├── enable-workflows.yaml
│ ├── import.yaml
│ ├── refresh-all.yaml
│ ├── status.yaml
│ └── update-gha.yaml
├── .gitignore
├── DESCRIPTION
├── LICENSE
├── README.md
├── TODO.md
├── actions-sync.Rproj
├── bin
├── add_worktrees
├── copy_templates
├── import
├── import_base
├── merge_into_remote
├── refresh_all
├── remove_worktrees
├── wt_copy_to
├── wt_git
├── wt_git_serial
├── wt_git_some
├── wt_pull
├── wt_run
└── wt_run_serial
├── index.Rmd
├── lib
└── lib.sh
├── run.sh
└── template
└── .github
└── workflows
└── push-on-change.yaml
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^.*\.Rproj$
2 | ^\.Rproj\.user$
3 |
--------------------------------------------------------------------------------
/.github/workflows/enable-workflows.yaml:
--------------------------------------------------------------------------------
1 | name: Enable workflows
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: '0 2 1 * *'
7 |
8 | jobs:
9 | enable-workflows:
10 | runs-on: ubuntu-22.04
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v4
15 | with:
16 | ref: main
17 | fetch-depth: 0
18 |
19 | - name: Get branches to enable
20 | run: |
21 | git branch -r | grep -E '.+/.+/.+' | grep -v HEAD | sed 's/origin\///' > repo_list
22 |
23 | - name: Enable workflows
24 | run: |
25 | while read line; do
26 | curl --fail-with-body -X PUT \
27 | -H "Accept: application/vnd.github+json" \
28 | -H "Authorization: token ${{ secrets.TOKEN_KEYS }}" \
29 | https://api.github.com/repos/$line/actions/workflows/pkgdown.yaml/enable
30 | curl --fail-with-body -X PUT \
31 | -H "Accept: application/vnd.github+json" \
32 | -H "Authorization: token ${{ secrets.TOKEN_KEYS }}" \
33 | https://api.github.com/repos/$line/actions/workflows/R-CMD-check.yaml/enable
34 | curl --fail-with-body -X PUT \
35 | -H "Accept: application/vnd.github+json" \
36 | -H "Authorization: token ${{ secrets.TOKEN_KEYS }}" \
37 | https://api.github.com/repos/$line/actions/workflows/R-CMD-check-dev.yaml/enable
38 | done < repo_list
39 |
--------------------------------------------------------------------------------
/.github/workflows/import.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | workflow_dispatch:
3 | inputs:
4 | remote_repo:
5 | description: 'Remote repository (owner/repo)'
6 | required: true
7 | base:
8 | description: 'Base repository (owner/repo) if remote repo has no workflows'
9 | required: false
10 |
11 | name: Import/refresh remote repositories
12 |
13 | jobs:
14 | import_repo:
15 | runs-on: ubuntu-24.04
16 |
17 | name: Import/refresh ${{ github.ref }} ${{ github.event.inputs.remote_repo }}
18 |
19 | steps:
20 | - name: Install git-filter-repo
21 | run: |
22 | sudo apt-get update
23 | sudo apt-get install -y git-filter-repo
24 |
25 | - name: Check out repository
26 | if: ${{ github.event.inputs.base }} == ''
27 | uses: actions/checkout@v4
28 | with:
29 | ref: main
30 | token: ${{ secrets.TOKEN_KEYS }}
31 |
32 | - name: Check out repository
33 | if: ${{ github.event.inputs.base }} != ''
34 | uses: actions/checkout@v4
35 | with:
36 | ref: main
37 | fetch-depth: 0
38 | token: ${{ secrets.TOKEN_KEYS }}
39 |
40 | - name: Configure Git identity
41 | run: |
42 | git log -n 1 --pretty=format:"git config --global user.name '%an' && git config --global user.email '%ae'" | tee /dev/stderr | sh
43 |
44 | - name: Check if branch exists
45 | id: check_branch
46 | run: |
47 | echo "exists=$(git branch --list origin/${{ github.event.inputs.remote_repo }})" >> $GITHUB_OUTPUT
48 |
49 | - name: Show if branch exists
50 | run: |
51 | echo ${{ steps.check_branch.outputs.exists }}
52 |
53 | - name: Import
54 | env:
55 | TOKEN_KEYS: ${{ secrets.TOKEN_KEYS }}
56 | run: |
57 | ./run.sh import_base "${{ github.event.inputs.remote_repo }}" "${{ github.event.inputs.base }}" --force
58 |
59 | - name: Trigger if branch is new
60 | if: ${{ steps.check_branch.outputs.exists }} == ""
61 | run: |
62 | echo ${{ secrets.TOKEN_KEYS }} | gh auth login --with-token
63 | gh workflow run push-on-change.yaml -r "${{ github.event.inputs.remote_repo }}"
64 | gh workflow run status.yaml
65 |
--------------------------------------------------------------------------------
/.github/workflows/refresh-all.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | schedule:
3 | - cron: '0 0 * * *'
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - main
8 | paths:
9 | - 'lib/**'
10 |
11 | name: Refresh all remote repositories
12 |
13 | jobs:
14 | import_repo:
15 | runs-on: ubuntu-24.04
16 |
17 | name: Refresh all remote repositories
18 |
19 | steps:
20 | - name: Install git-filter-repo
21 | run: |
22 | sudo apt-get update
23 | sudo apt-get install -y git-filter-repo
24 |
25 | - name: Check out repository
26 | uses: actions/checkout@v4
27 | with:
28 | ref: main
29 | fetch-depth: 0
30 | token: ${{ secrets.TOKEN_KEYS }}
31 |
32 | - name: Configure Git identity
33 | run: |
34 | git log -n 1 --pretty=format:"git config --global user.name '%an' && git config --global user.email '%ae'" | tee /dev/stderr | sh
35 |
36 | - name: Import
37 | env:
38 | TOKEN_KEYS: ${{ secrets.TOKEN_KEYS }}
39 | run: |
40 | ./run.sh refresh_all || ./run.sh refresh_all || ./run.sh refresh_all
41 |
42 | - name: Show job log
43 | if: always()
44 | run: |
45 | cat refresh_all.log
46 |
--------------------------------------------------------------------------------
/.github/workflows/status.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | paths:
6 | - .github/workflows/status.yaml
7 | - index.Rmd
8 | workflow_dispatch:
9 | schedule:
10 | - cron: '20 2 * * *'
11 |
12 | name: Status
13 |
14 | jobs:
15 | status:
16 | runs-on: ubuntu-22.04
17 |
18 | # Begin custom: services
19 | # End custom: services
20 | env:
21 | RSPM: "https://packagemanager.rstudio.com/cran/__linux__/jammy/latest"
22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
23 | # prevent rgl issues because no X11 display is available
24 | RGL_USE_NULL: true
25 | # Begin custom: env vars
26 | # End custom: env vars
27 |
28 | steps:
29 | - name: Check rate limits
30 | run: |
31 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit
32 | shell: bash
33 |
34 | - uses: actions/checkout@v4
35 |
36 | - uses: r-lib/actions/setup-r@v2
37 | with:
38 | install-r: false
39 |
40 | - uses: r-lib/actions/setup-pandoc@v2
41 |
42 | - name: Install remotes
43 | run: |
44 | if (!requireNamespace("curl", quietly = TRUE)) install.packages("curl")
45 | if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes")
46 | shell: Rscript {0}
47 |
48 | - name: Query dependencies
49 | run: |
50 | saveRDS(remotes::dev_package_deps(dependencies = TRUE, type = .Platform$pkgType), ".github/depends.Rds", version = 2)
51 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version")
52 | shell: Rscript {0}
53 |
54 | - name: Restore R package cache
55 | uses: actions/cache@v4
56 | with:
57 | path: ${{ env.R_LIBS_USER }}
58 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-3-${{ hashFiles('.github/depends.Rds') }}
59 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-
60 |
61 | - name: Configure Git identity
62 | run: |
63 | env | sort
64 | git config --global user.name "$GITHUB_ACTOR"
65 | git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com"
66 | shell: bash
67 |
68 | - name: Install system dependencies
69 | if: runner.os == 'Linux'
70 | run: |
71 | sudo apt-get update -y
72 | while read -r cmd
73 | do
74 | eval sudo $cmd
75 | done < <(Rscript -e 'req <- remotes::system_requirements("ubuntu", "22.04"); if (length(req) > 0) cat(req, sep = "\n")')
76 |
77 | # Begin custom: before install
78 | # End custom: before install
79 |
80 | - name: Install pkgdown sysdeps
81 | if: runner.os == 'Linux'
82 | env:
83 | RHUB_PLATFORM: linux-x86_64-ubuntu-gcc
84 | run: |
85 | # For some reason harfbuzz and gert are installed from source and needs this
86 | sudo apt-get install -y libharfbuzz-dev libfribidi-dev libgit2-dev
87 |
88 | - name: Install dependencies
89 | run: |
90 | remotes::install_deps(dependencies = TRUE, type = .Platform$pkgType)
91 | shell: Rscript {0}
92 |
93 | # Begin custom: install
94 | # End custom: install
95 |
96 | - name: Session info
97 | run: |
98 | options(width = 100)
99 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo")
100 | pkgs <- installed.packages()[, "Package"]
101 | sessioninfo::session_info(pkgs, include_base = TRUE)
102 | shell: Rscript {0}
103 |
104 | # Begin custom: after install
105 | # End custom: after install
106 |
107 | - name: Clone worktrees
108 | run: |
109 | git fetch --depth=1
110 | ./run.sh add_worktrees
111 | shell: bash
112 |
113 | - name: Create gh-pages worktree
114 | run: |
115 | if ! git show-branch remotes/origin/gh-pages; then
116 | mkdir -p gh-pages
117 | cd gh-pages
118 | git init
119 | git commit -m Initial --allow-empty
120 | git remote add up ..
121 | git push up HEAD:gh-pages
122 | cd ..
123 | rm -rf gh-pages
124 | git push -u origin gh-pages
125 | fi
126 | git worktree add gh-pages gh-pages
127 | shell: bash
128 |
129 | - name: Build site
130 | run: |
131 | rmarkdown::render("index.Rmd", output_dir = "gh-pages")
132 | shell: Rscript {0}
133 |
134 | - name: Deploy site
135 | run: |
136 | cd gh-pages
137 | git fetch --depth=1
138 | git reset origin/gh-pages
139 |
140 | if [ $(git status --porcelain | wc -l) -gt 0 ]; then
141 | git add .
142 | git commit -m "Update site"
143 | git push
144 | fi
145 | shell: bash
146 |
147 | - name: Check rate limits
148 | if: always()
149 | run: |
150 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit
151 | shell: bash
152 |
--------------------------------------------------------------------------------
/.github/workflows/update-gha.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | paths:
6 | - '.github/workflows/update-gha.yaml'
7 | - 'template/**'
8 |
9 | name: Update GHA in remote repositories
10 |
11 | jobs:
12 | create_key:
13 | runs-on: ubuntu-22.04
14 |
15 | name: Update GHA in remote repositories
16 |
17 | steps:
18 | - name: Check out remote repository
19 | uses: actions/checkout@v4
20 | with:
21 | fetch-depth: 0
22 | token: ${{ secrets.TOKEN_KEYS }}
23 |
24 | - name: Configure Git identity
25 | run: |
26 | git log HEAD^.. --pretty=format:"git config --global user.name '%an' && git config --global user.email '%ae'" | tee /dev/stderr | sh
27 |
28 | - name: Update
29 | run: |
30 | ./run.sh copy_templates
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | wt/
2 | index.html
3 | index_files/
4 | .Rproj.user
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: actions.status
2 | Title: GitHub Actions Status Badges
3 | Version: 0.0.0.9000
4 | Date: 2021-03-14
5 | Authors@R:
6 | person(given = "Kirill",
7 | family = "Müller",
8 | role = c("aut", "cre"),
9 | email = "krlmlr+r@mailbox.org",
10 | comment = c(ORCID = "0000-0002-1416-3412"))
11 | Description: Collects status badges for GitHub Actions projects.
12 | License: GPL-3
13 | URL: https://github.com/krlmlr/actions-sync,
14 | https://krlmlr.github.io/actions-sync
15 | BugReports: https://github.com/krlmlr/actions-sync/issues
16 | Imports:
17 | dplyr,
18 | purrr,
19 | rmarkdown
20 | Encoding: UTF-8
21 | LazyData: true
22 | Roxygen: list(markdown = TRUE)
23 | RoxygenNote: 7.1.1.9001
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [actions-sync](https://krlmlr.github.io/actions-sync)
2 |
3 | Manage all your GitHub Actions workflows across multiple projects.
4 | Synchronize to and from all your projects.
5 | Apply changes to similar workflows at once across all your projects.
6 |
7 | ## Setup
8 |
9 | 1. Create a repository from this template using the green "Use this template" button at the top right
10 | - Leave "Include all branches" unchecked.
11 | - You can make this repository public, or keep it private.
12 | 2. Create a PAT and store it as a secret.
13 | 1. Visit
14 | 2. Click "Generate new token (fine-grained)" with scopes:
15 | - **actions** (read-write, needed to trigger workflows in other repos)
16 | - **contents** (read-write, needed for private repositories)
17 | - **metadata** (read-only, on by default)
18 | - **workflow** (read-write, needed to change workflows in other repos)
19 | 3. Copy generated token
20 | 4. Store it as a secret named `TOKEN_KEYS`
21 | 5. Remember to update the token when it expires
22 | 3. Clone your new repository locally
23 | - To be able to benefit from changes in this repository, run the following code in your terminal:
24 | ```r
25 | git remote add krlmlr https://github.com/krlmlr/actions-sync
26 | git fetch --all
27 | git merge krlmlr/main -s ours --allow-unrelated-histories
28 | git push
29 | ```
30 |
31 |
32 | ## Design
33 |
34 | Branches in this *central repository* correspond to projects (*remote repositories*) on GitHub.
35 | Each branch here contains the history of `.github/workflows` in the corresponding remote repository.
36 | Existing projects can be imported with their history.
37 | Projects that don't have GitHub Actions yet can inherit from an existing project by creating a new branch from an existing branch in the central repository.
38 |
39 | From then on, pushes to the central repository apply the new commits to the remote repository, with a technique similar to `git subtree`.
40 | Backwards synchronization happens on schedule and is a variant of the initial import.
41 | Whenever the code in the central repository is identical to the remote code, a full import of the remote history is carried out.
42 | If the remote code is different (e.g. if you changed the actions directly in the remote repository), an attempt is made to isolate the commits from the remote history and to apply them here.
43 |
44 | Branches that start with `main` are special, so is the `gh-pages` branch.
45 | Also, branches that don't have a slash in their name are not synchronized with repositories.
46 |
47 | ### Is it safe?
48 |
49 | The central repository never performs force-push or delete actions to remote repositories.
50 | Workflows in remote repositories will contain a history of all changes that came from the central repository.
51 |
52 | ### Example
53 |
54 | > Example: I maintain [r-dbi/DBI](https://github.com/r-dbi/DBI), [r-dbi/RKazam](https://github.com/r-dbi/RKazam) and [r-lib/rprojroot](https://github.com/r-lib/rprojroot), among other projects.
55 | > The central repository has branches:
56 | >
57 | > - [r-dbi/DBI](https://github.com/krlmlr/actions-sync/tree/r-dbi/DBI)
58 | > - [r-dbi/RKazam](https://github.com/krlmlr/actions-sync/tree/r-dbi/RKazam)
59 | > - [r-lib/rprojroot](https://github.com/krlmlr/actions-sync/tree/r-lib/rprojroot)
60 | > - ...
61 | >
62 | > The top level of these branches contain the `.yaml` files from the `.github/workflows` directory in the remote repositories.
63 | > It also contains its own `.github` directory that powers the synchronization but is not copied to the remote repository.
64 | >
65 | > An overview page over all workflows in remote repositories, updated daily, is deployed to .
66 |
67 | ## Basic workflow
68 |
69 | 1. Import a project, one of:
70 | 1. Trigger the ["Import/refresh remote repositories" action](https://github.com/krlmlr/actions-sync/actions?query=workflow%3A%22Import%2Frefresh+remote+repositories%22): click "Run workflow", enter the owner/repo of the repository you want to import
71 | 1. `bin/import owner/repo`
72 | 1. Copy the setup for an existing *base* project to a new project, one of:
73 | 1. Trigger the ["Import/refresh remote repositories" action](https://github.com/krlmlr/actions-sync/actions?query=workflow%3A%22Import%2Frefresh+remote+repositories%22): click "Run workflow", enter the owner/repo of the repository you want to import and the name of the base owner/repo of the boilerplate repository
74 | 1. `bin/import_base owner/repo base-owner/base-repo`
75 | 1. Synchronization from this repository to the remote repositories:
76 | - automatically via GitHub Actions, on push
77 | 1. Synchronization from the remote repositories to this repository:
78 | - automatically via GitHub Actions, on schedule or triggered
79 |
80 | There are also manual ways to synchronize but this will bork your work trees.
81 | Use with care!
82 |
83 | ## Editing workflows locally
84 |
85 | Tested on Ubuntu.
86 | Requires GNU `parallel`.
87 |
88 | 1. Extract all branches as worktrees locally, to the `wt/` directory:
89 |
90 | ```sh
91 | bin/add_worktrees
92 | ```
93 |
94 | 1. Check history by date in all worktrees, to remind you which actions have been updated recently in which remote repository:
95 |
96 | ```sh
97 | git log --all --graph --date-order
98 | ```
99 |
100 | 1. Apply a Git command to all worktrees:
101 | - Fast-forward pull all worktrees:
102 |
103 | ```sh
104 | bin/wt_pull
105 | ```
106 |
107 | - Rebase all worktrees:
108 |
109 | ```sh
110 | git fetch
111 | bin/wt_git rebase
112 | ```
113 |
114 | - Show status for all worktrees:
115 |
116 | ```sh
117 | bin/wt_git status
118 | ```
119 |
120 | - Show diff for all worktrees:
121 |
122 | ```sh
123 | bin/wt_git diff
124 | ```
125 |
126 | - Commit all worktrees:
127 |
128 | ```sh
129 | bin/wt_git commit -m "My commit message"
130 | ```
131 |
132 | - Alternatively, if you want to cherry-pick a commit and apply to all worktrees
133 |
134 | ```sh
135 | bin/wt_git cherry-pick commitSha
136 | ```
137 |
138 | 1. Push all worktrees:
139 |
140 | ```sh
141 | git push --all
142 | ```
143 |
144 | - You can also add `-n` to verify what happens
145 |
146 | ```sh
147 | git push --all -n
148 | ```
149 |
150 | - After push to this repository, the contents are synchronized with the remote repository by GitHub Actions
151 |
152 | 1. Clean up all worktrees
153 |
154 | ```sh
155 | bin/remove_worktrees
156 | ```
157 |
158 | 1. Run operations for some worktrees (example: starting with `pois`)
159 |
160 | ```sh
161 | bin/wt_git_some pois status
162 | ```
163 |
164 |
165 | ## Maintaining similar yet different workflows across projects
166 |
167 | For R projects, workflows may differ across projects:
168 |
169 | - Success conditions for CI may be loosened for some projects or on some R versions
170 | - Additional software may need to be installed for some projects
171 | - The test matrix may contain additional entries for some projects
172 |
173 | This will be similar also for other environments.
174 |
175 | To make this maintainable in the longer term, I use a `base` branch that contains the common parts.
176 | Extension points are placed in the `.yaml` files surrounded by "# Begin custom:" and "# End custom:" comments.
177 | If the base workflow changes, most of the time the change can be cherry-picked into the project branches without conflicts: the comments serve as anchors that isolate the custom from the common parts.
178 |
179 | - Apply the last commit of the `base` branch to all other branches:
180 |
181 | ```sh
182 | bin/wt_git cherry-pick base
183 | ```
184 |
185 | - Apply the last three commits of the `base` branch to all other branches:
186 |
187 | ```sh
188 | bin/wt_git cherry-pick base~3..base
189 | ```
190 |
191 | If a workflow in a remote repository changes common parts, they are brought back into the `base` branch.
192 |
193 | ```sh
194 | cd wt/base
195 | git checkout -f owner/repo -- .
196 | # Keep only desired changes
197 | git add .
198 | git commit -f
199 | cd ../..
200 | ```
201 |
202 | For a "tabula rasa" style setup, we can also overwrite worktrees with the contents of a branch.
203 |
204 | - Copy over `workflow.yaml` from the `base` branch to all other branches:
205 |
206 | ```sh
207 | bin/wt_git checkout -f base -- workflow.yaml
208 | ```
209 |
210 | - Copy over the contents of the `base` branch to all other branches:
211 |
212 | ```sh
213 | bin/wt_copy_to base-branch
214 | ```
215 |
216 | ## Troubleshooting
217 |
218 | If the synchronization breaks for one or multiple repositories, remove the corresponding repository branch and re-add.
219 |
220 | ## Hacking
221 |
222 | Code is in `lib/lib.sh`.
223 | Public functions don't start with an underscore and have a comment on the line of the function definition.
224 | This is used for creating the command scripts and for the usage.
225 |
226 | ### Update command scripts
227 |
228 | ```sh
229 | ./run.sh _make_commands
230 | ```
231 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | - Fix failing workflows
4 | - Add overview of build statuses for all projects
5 | - Harmonize
6 | - Show testthat output -> show test output
7 | - Decide on R 3.2
8 | - Add code to prune branches in fork
9 | - Add workflow to create commands
10 | - Add code to sync/copy base to other branches
11 | - BUG: Repositories cannot be named main, fix grep expressions searching for `main`
12 |
13 | ## Later
14 |
15 | - Remove hard-coded github.com
16 | - Use git-filter-repo
17 |
18 | ## Caveat
19 |
20 | - Sync from remote repos overwrites history due to change in committer date. Can we cherry-pick with the original committer date? Do we need a low-level Git command?
21 | - Problem: commit from internal `.github/workflows`:
22 |
23 | ```text
24 | * commit 5a4351d22b75280073c26cbf58bd145a8b49600f (origin/r-prof/winch)
25 | | Author: Kirill Müller
26 | | AuthorDate: Tue Dec 8 07:37:07 2020 +0000
27 | | Commit: Kirill Müller
28 | | CommitDate: Tue Dec 8 07:37:07 2020 +0000
29 | |
30 | | Update push action
31 | |
32 | | 35 0 .github/workflows/push-on-change.yaml
33 | |
34 | | * commit 56ddb0b20f1bdf08681476e25c36c7be1b6096ff (r-prof/winch)
35 | |/ Author: Kirill Müller
36 | | AuthorDate: Tue Dec 8 00:44:40 2020 +0000
37 | | Commit: Kirill Müller
38 | | CommitDate: Tue Dec 8 00:44:40 2020 +0000
39 | |
40 | | Update push action
41 | |
42 | | 35 0 .github/workflows/push-on-change.yaml
43 | |
44 | ```
45 |
46 | - Solved by double rebase with fallback to force-push
47 |
48 | - Merges in local history can't be easily recreated in the remote repos
49 |
50 | - Push to `templates/` then unrelated push doesn't trigger workflow?
51 |
52 | ## Done
53 |
54 | - How to remove?
55 | - Remove branch, no foreign workflows
56 | - Switch to scheduled sync back
57 | - Add local sync back action to every branch: doesn't work, scheduled works only for main branch
58 | - Think about sync back on remote change
59 | - No
60 | - Implement merge back
61 | - With import, refresh=FALSE
62 | - Test with RKazam
63 | - How to resend secret to remote repository -- manual workflow run?
64 | - Trigger resend of secret when key is regenerated
65 |
--------------------------------------------------------------------------------
/actions-sync.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 |
3 | RestoreWorkspace: No
4 | SaveWorkspace: No
5 | AlwaysSaveHistory: No
6 |
7 | EnableCodeIndexing: Yes
8 | UseSpacesForTab: Yes
9 | NumSpacesForTab: 2
10 | Encoding: UTF-8
11 |
12 | RnwWeave: Sweave
13 | LaTeX: XeLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Package
19 | PackageUseDevtools: Yes
20 | PackageInstallArgs: --no-multiarch --with-keep.source
21 |
22 | QuitChildProcessesOnExit: Yes
23 | DisableExecuteRprofile: Yes
24 |
--------------------------------------------------------------------------------
/bin/add_worktrees:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Add all branches representing workflows in foreign repositories as worktrees in the wt/ directory"
6 |
7 | add_worktrees "$@"
8 |
--------------------------------------------------------------------------------
/bin/copy_templates:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Copy workflow templates into foreign repositories"
6 |
7 | copy_templates "$@"
8 |
--------------------------------------------------------------------------------
/bin/import:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Import a new repository, pass slug as argument"
6 |
7 | import "$@"
8 |
--------------------------------------------------------------------------------
/bin/import_base:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Import a new repository with fallback to a base branch, pass slug and base branch as argument"
6 |
7 | import_base "$@"
8 |
--------------------------------------------------------------------------------
/bin/merge_into_remote:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Merge our workflow into the remote repository. Makes worktree unusable. Takes the slug as argument"
6 |
7 | merge_into_remote "$@"
8 |
--------------------------------------------------------------------------------
/bin/refresh_all:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Refresh all repositories"
6 |
7 | refresh_all "$@"
8 |
--------------------------------------------------------------------------------
/bin/remove_worktrees:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Remove wt/ directory and all local branches. Potentially destructive, check output!"
6 |
7 | remove_worktrees "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_copy_to:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Copy a branch into all worktrees, don't commit. Pass local branch as argument"
6 |
7 | wt_copy_to "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_git:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Run git command in all worktrees"
6 |
7 | wt_git "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_git_serial:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Run git command in all worktrees, with terminal support"
6 |
7 | wt_git_serial "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_git_some:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Run git command in all worktrees"
6 |
7 | wt_git_some "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_pull:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Run git pull --ff-only for all worktrees"
6 |
7 | wt_pull "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_run:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Run command in all worktrees, use '{}' as placeholder for worktree directory"
6 |
7 | wt_run "$@"
8 |
--------------------------------------------------------------------------------
/bin/wt_run_serial:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . lib/lib.sh
4 |
5 | # "Run command in all worktrees, use '{}' as placeholder for worktree directory"
6 |
7 | wt_run_serial "$@"
8 |
--------------------------------------------------------------------------------
/index.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | output:
3 | html_document:
4 | self_contained: false
5 | title: "Repository status"
6 | ---
7 |
8 | ```{r include = FALSE}
9 | github <- Sys.getenv("GITHUB_SERVER_URL", "https://github.com")
10 | gen_slug <- Sys.getenv("GITHUB_REPOSITORY")
11 |
12 | library(dplyr)
13 | library(purrr)
14 | ```
15 |
16 | Generated by [`r gen_slug`](`r github`/`r gen_slug`).
17 |
18 |
19 | ```{r echo = FALSE}
20 | all_workflows <- dir("wt", recursive = TRUE)
21 | repo_workflows <- grep("^[^/]+/[^/]+/[^/]+[.]ya?ml$", all_workflows, value = TRUE)
22 |
23 | has_push <- function(x) {
24 | if (is.character(x)) {
25 | "push" %in% x
26 | } else {
27 | !is.null(x$push)
28 | }
29 | }
30 |
31 | make_slug <- function(slug) {
32 | paste0(
33 | '',
34 | slug,
35 | "",
36 | ' ',
37 | "(actions)",
38 | ""
39 | )
40 | }
41 |
42 | make_badge <- function(slug, name, yaml_name) {
43 | paste0(
44 | '',
45 | "![", name, "]",
46 | "(https://shields.io/github/actions/workflow/status/", slug, "/", yaml_name, "?label=", name, ")",
47 | ""
48 | )
49 | }
50 |
51 | tibble(workflow = repo_workflows) %>%
52 | mutate(contents = map(file.path("wt", workflow), ~ suppressWarnings(yaml::read_yaml(.x, eval.expr = FALSE)))) %>%
53 | mutate(has_push = map_lgl(contents, ~ has_push(.x$on) || has_push(.x$"TRUE"))) %>%
54 | filter(has_push) %>%
55 | mutate(name = map_chr(contents, c("name"))) %>%
56 | mutate(yaml_name = basename(workflow)) %>%
57 | mutate(name = coalesce(name, yaml_name)) %>%
58 | mutate(slug = dirname(workflow)) %>%
59 | add_count(name) %>%
60 | arrange(slug, desc(n), name) %>%
61 | select(slug, name, yaml_name) %>%
62 | group_by(slug) %>%
63 | summarize(badge = paste(make_badge(slug, name, yaml_name), collapse = " ")) %>%
64 | ungroup() %>%
65 | transmute(Repository = make_slug(slug), Badges = badge) %>%
66 | knitr::kable()
67 | ```
68 |
--------------------------------------------------------------------------------
/lib/lib.sh:
--------------------------------------------------------------------------------
1 | set -e
2 |
3 | _make_commands() {
4 | rm -rf bin
5 | mkdir bin
6 |
7 | cat > run.sh <<"EOF"
8 | #!/bin/bash
9 |
10 | . lib/lib.sh
11 |
12 | if [ "$1" = "" ]; then
13 | echo "Usage: $0 command ..."
14 | echo
15 | echo "with command one of:"
16 | echo
17 | gsed -r -n '/^([a-z].*)[(][)] [{] +# (.*)$/ { s//- \1: \2/; p }' lib/lib.sh
18 | return 1
19 | fi
20 |
21 | echo "> $1"
22 |
23 | "$@"
24 | EOF
25 | chmod +x run.sh
26 |
27 | gsed -r -n '/^([a-z].*)[(][)] [{] +# (.*)$/ { s//\1 "\2"/; p }' lib/lib.sh | parallel ./run.sh _make_command
28 | }
29 |
30 | _make_command() {
31 | _make_command_uq $@
32 | }
33 |
34 | _make_command_uq() {
35 | command=$1
36 | shift
37 | comment="$@"
38 |
39 | echo ${command}
40 |
41 | cat > bin/${command} <" "$@"
15 |
16 | "$@"
17 |
--------------------------------------------------------------------------------
/template/.github/workflows/push-on-change.yaml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - '*/*'
5 | workflow_dispatch:
6 |
7 | name: Push on change
8 |
9 | jobs:
10 | create_key:
11 | runs-on: ubuntu-24.04
12 |
13 | name: Push on change ${{ github.ref }}
14 |
15 | steps:
16 | - name: Install git-filter-repo
17 | run: |
18 | sudo apt-get update
19 | sudo apt-get install -y git-filter-repo
20 |
21 | - name: Check out our repository
22 | uses: actions/checkout@v4
23 | with:
24 | ref: main
25 | fetch-depth: 0
26 |
27 | - name: Configure Git identity
28 | run: |
29 | git log -n 1 --pretty=format:"git config --global user.name '%an' && git config --global user.email '%ae'" | tee /dev/stderr | sh
30 |
31 | - name: Get branch name
32 | id: remote_repo
33 | run: |
34 | echo "slug=$(echo $GITHUB_REF | sed 's#refs/heads/##')" >> $GITHUB_OUTPUT
35 | shell: bash
36 |
37 | - name: Merge subtree
38 | env:
39 | TOKEN_KEYS: ${{ secrets.TOKEN_KEYS }}
40 | GH_TOKEN: ${{ secrets.TOKEN_KEYS }}
41 | run: |
42 | ./run.sh merge_into_remote ${{ steps.remote_repo.outputs.slug }}
43 |
--------------------------------------------------------------------------------