├── .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 | --------------------------------------------------------------------------------