Skip to content

Start turning GitGitGadget into a set of GitHub Actions and workflows #1392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 40 commits into
base: main
Choose a base branch
from

Conversation

dscho
Copy link
Member

@dscho dscho commented Aug 20, 2023

This is based on the discussion at #609.

Migration Plan: Azure Pipelines to GitHub Actions

This document outlines the strategy for migrating GitGitGadget's automation from Azure Pipelines to GitHub Actions.

Background & Motivation

For years, we have relied on a set of Azure Pipelines backed by a database maintained in Git notes. This global state required careful handling of concurrency, which we solved by running all jobs inside a dedicated Azure Pipeline runner on a single Azure VM. This setup guaranteed exclusive access, allowing the pipeline to read, update, and push Git notes without concurrent interference.

However, this approach is fragile and problematic:

  • During a lengthy Azure Pipeline run (looking at you, Update GitGitGadget's PRs, no other Azure Pipeline can run, which often results in delays of, say, /submiting patch series.
  • It depends on the self-hosted runner and on my personal Azure account.
  • The Pipelines are not YAML-based, making modifications less transparent and contributions impossible.
  • The current setup is full of hacks to accommodate for compatibility issues with certain Git versions (that I have to upgrade manually on that runner) and various other issues I had to address over the years.
  • Keeping the GitGitGadget checkout up to date on that runner, and transpiled, adds to some unwanted complexity.
  • Persistent repositories between runs can retain unwanted information, and are susceptible to attacks where one workflow run (that has to accept untrusted input) could lead to a change of behavior that persists when running subsequent workflow runs (including those runs that are supposed to be safe from untrusted input and has access to sensitive credentials).

Project Goal

My overarching goal is to make GitGitGadget more robust, maintainable, and contributor-friendly by migrating to GitHub Actions and workflows.

Migration Approach

  • Restructure GitGitGadget's source code so it can be executed via GitHub Actions. All functionality currently in Azure Pipelines will be available in GitHub Actions, with each Action corresponding to one or more of the existing Azure Pipelines.
  • The main codebase remains in TypeScript, transpiled to JavaScript. The CIHelper class serves as the main entry point.
  • Each GitHub Action is implemented as minimal JavaScript, delegating all core logic to the CI helper.
  • The goal is for each GitHub Action to consist of a minimal action.yml and index.js.
  • Each GitHub Action is called by exactly one, minimal GitHub workflow, to reduce the complexity of the new approach.

GitHub Actions & Workflows

  • When code is updated in the main branch, a GitHub workflow in gitgitgadget/gitgitgadget updates a dedicated v1 branch. This branch contains the full commit history of the main branch (via merging it) but only the transpiled and bundled JavaScript at its tip commit's tree, ready for use as a GitHub Action.
  • Versioning is more transparent: Each workflow run updates the v1 branch and maintains an incremental v1.<number> tag.
  • GitHub Actions must be triggered by workflows. To keep maintenance simple, each Action will have a corresponding minimal workflow that calls only that Action. These workflows reside in the gitgitgadget-workflows org.

Concurrency Considerations

  • Running the workflows in a separate GitHub org addresses concurrency limits, especially when those large pushes by the Git maintainer where dozens of branches are pushed at the same time trigger as many CI runs that each require a whopping 8-12h overall and blocks all other use of GitHub Actions for hours on end.

PR Checks Integration

  • One drawback of the migration is losing the PR checks that are provided automatically by Azure Pipelines. To restore this convenience, I plan on developing a new GitHub Action that mirrors check runs from gitgitgadget-workflows to the respective PRs. This approach has been successfully used in the Git for Windows project, and those learnings will be invaluable.

Detailed overview of the new GitHub Actions

  • handle-pr-comment
  • handle-pr-push
  • handle-new-mails
  • update-prs
    • replaces:
      1. Update GitGitGadget's PRs
    • triggers on: seen updates
    • runs: updateOpenPrs(), updateCommitMappings() and handleOpenPRs()
  • update-mail-to-commit-notes
    • replaces:
      1. Update GitGitGadget's commit to mail notes
    • triggers on: seen updates
    • TODO: convert from using gitster/seen to upstream/seen
    • needs: git-mailing-list
    • runs:
      rev="$(git -C "$GITGIT_DIR" rev-parse refs/notes/commit-to-mail)"
      ./gitgitgadget/script/lookup-commit.sh --notes update
      if test "$rev" != "$(git -C "$GITGIT_DIR" rev-parse refs/notes/commit-to-mail)"
      then
        ./gitgitgadget/script/update-mail-to-commit-notes.sh
      
        git -C "$GITGIT_DIR" push https://git-for-windows-ci:$(git-for-windows-ci.github.token)@github.com/gitgitgadget/git refs/notes/commit-to-mail refs/notes/mail-to-commit ||
        die "Could not push notes"
      
        no_match="$(git -C "$GITGIT_DIR"/ show refs/notes/commit-to-mail | grep -B2 no\ match | sed -ne 's/\///g' -e 's/^\+\+\+ b//p')"
        test -z "$no_match" ||
        die "Could not find mail(s) for: $no_match"
      fi
      

Oh, and best of all: the way the Azure Pipelines run GitGitGadget via misc-helper will continue working all the way through the migration.

@dscho
Copy link
Member Author

dscho commented Aug 20, 2023

To verify that it all works as intended, I added a commit to add a reaction to dscho/git#29 (comment) (the commit will be dropped before long, of course, it's intention was only to demonstrate that the design works).

There were a few linting issues I had missed, so I added another reaction (via https://github.com/gitgitgadget/gitgitgadget/actions/runs/5916258877).

@webstech
Copy link
Contributor

webstech commented Sep 7, 2023

fyi, I am reviewing this. Just taking some time.

@dscho
Copy link
Member Author

dscho commented Sep 7, 2023

@webstech no worries!

@dscho
Copy link
Member Author

dscho commented Nov 8, 2023

This PR will need to be updated using #1473 once that one is merged, to be used in a post-Action of the init one to ensure that the local refs/notes/gitgitgadget state is synchronized back to https://github.com/gitgitgadget/git.

@dscho dscho force-pushed the github-action branch 4 times, most recently from 2f31cb4 to fac3d88 Compare February 8, 2025 13:56
@dscho dscho force-pushed the github-action branch 2 times, most recently from fbb2ca1 to 992be41 Compare March 4, 2025 12:33
@dscho dscho mentioned this pull request Aug 15, 2025
dscho added 4 commits August 15, 2025 21:52
Network calls in tests not only make everything a lot slower than
necessary, but also add an undesirable source of test failures. So let's
avoid them.

Signed-off-by: Johannes Schindelin <[email protected]>
When testing whether a given `/allow` command yields the expected
result, let's mock out the actual GitHub REST API call.

Signed-off-by: Johannes Schindelin <[email protected]>
It really needs to be reusable instead of being stuck in the script. In
particular since we're about to modify GitGitGadget such that it can be
run as a GitHub Action instead.

Signed-off-by: Johannes Schindelin <[email protected]>
@dscho dscho force-pushed the github-action branch 3 times, most recently from ec83f79 to 636881b Compare August 15, 2025 22:23
It was a bit hard for me to follow the code, getting confused by two
separate `getConfig()` functions that do different things (the one from
`gitgitgadget-config` initializes the default config, the one from
`project-config` gets a singleton that might still be uninitialized).

Let's unconfuse things by making the config a parameter of the
constructor. It can still be omitted, in which case the default config
is used.

Note: This commit is only concerned with the CIHelper class, because we
will use it from light-weight GitHub Actions in the future. The rest of
the code really needs to use the singleton up until the time when we'll
get around to The Big Refactor.

Signed-off-by: Johannes Schindelin <[email protected]>
@dscho dscho force-pushed the github-action branch 3 times, most recently from b2b129a to 43af519 Compare August 16, 2025 19:34
dscho added 2 commits August 16, 2025 19:37
When no `workDir` is specified, default to using the `git/` directory in
the current directory. This will be initialized as a bare repository in
the upcoming GitHub Actions.

Signed-off-by: Johannes Schindelin <[email protected]>
In #1974, I changed the
way the push token is handed down from CIHelper to GitGitGadget instead
of relying on the Git config (or environment) to specify it implicitly.
Let's do the same for the URL of the repository to which to push the Git
notes and patch series iterations' tags.

Signed-off-by: Johannes Schindelin <[email protected]>
dscho added 25 commits August 16, 2025 22:17
…Actions

Some of them need a local (partial) clone of the Git mailing list, most
don't, though. So let's make it easy for the Actions to specify what
they need.

Signed-off-by: Johannes Schindelin <[email protected]>
This will be used in the upcoming `handle-pr-comment` PR.

Signed-off-by: Johannes Schindelin <[email protected]>
This will be used to convert GitGitGadget to a GitHub Action.

Signed-off-by: Johannes Schindelin <[email protected]>
The plan is to turn GitGitGadget into a set of GitHub Actions that can
be run conveniently from GitHub workflows.

To that end, we will add subdirectories (e.g. `init/`, `handle-prs/`,
etc) that reflect the individual functionality that is currently called
in Azure Pipelines via subcommands of the `misc-helper` script.

Those subdirectories will contain the bare minimum required to implement
a Javascript Action that calls into the CIHelper class: an `action.yml`
file to declare the inputs/outputs, and a minimal `index.js` that
consumes the inputs and passes them to the CIHelper.

The CIHelper class will be used by the Action via as a single,
dependency-free `dist/index.js`, and it will be the only dependency of
the Action's `index.js`.

In particular, we must not let the Action's minimal `index.js` depend on
`@actions/core` because it simply won't be there.

So let's add it as a dependency of the CIHelper class and provide a
function to allow calling the imported `@actions/core` functions from
the Actions' `index.js`.

Signed-off-by: Johannes Schindelin <[email protected]>
Preparing for turning GitGitGadget into a bunch of GitHub Actions, we
install `@vercel/ncc`, a 'simple CLI for compiling a Node.js module into
a single file, together with all its dependencies, gcc-style.'. This
will allow us to bundle a minimal set of Javascript files and publish
the result via a tag.

The idea is to run `ncc build -s -m lib/ci-helper.ts -o dist/` to obtain
a minimized `dist/index.js` that has no additional dependencies, and
then have subdirectories (e.g. `update-prs/`) with the individual
`action.yml` and `index.js` that starts with the following line

	const { CIHelper } = require('../dist/index.js')

This will allow us to publish a single tag that backs the various
functionalities via different GitHub Actions, e.g:

	- uses: gitgitgadget/gitgitgadget/update-prs@v1
	  with:
	    config: ${{ vars.GITGITGADGET_CONFIG }}

Signed-off-by: Johannes Schindelin <[email protected]>
This adds a convenience `script` entry to the `package.json` file that
allows bundling everything into `dist/`, dependency-free.

Since `ncc` generates a lot of `.d.ts` files (which are not required to
run the GitHub Action), we go ahead and tell Git to ignore these.

Signed-off-by: Johannes Schindelin <[email protected]>
According to vercel/ncc#829, `ncc` has some
magic that includes the resource in the bundled package if the path is
specified using `path.join(__dirname, ...)`.

However, we're already using ESModule, therefore we need to use the
`import.meta.url` syntax (`import.meta.dirname` unfortunately does not
seem to work in my hands).

Having said that, the promise was that `ncc` would include this asset in
the output, but this is not true for me.

Signed-off-by: Johannes Schindelin <[email protected]>
I am about to add some JavaScript code, where semicolons are frequently
unneeded. Let's skip them there.

Signed-off-by: Johannes Schindelin <[email protected]>
The idea is to let `ncc` turn the CIHelper code into a dependency-free
`dist/index.js` and then remove all files other than `*/action.yml` and
`*/index.js`, commit and tag that. That's the GitHub Action, ready to
be used in GitHub workflows.

Since `ncc` produces an ECMAScript module, we cannot use the old-style
`require()` call but need to use `await import()` instead.

Signed-off-by: Johannes Schindelin <[email protected]>
This adds a reaction to a PR comment in my fork, to verify that
GitGitGadget can do such things when called via the new GitHub Action.

Signed-off-by: Johannes Schindelin <[email protected]>
This is the culmination of all the preparation: The first GitHub Action
that will be used in a GitHub workflow that will replace the trusty
Azure Pipelines we used for so long.

Signed-off-by: Johannes Schindelin <[email protected]>
When debugging locally, say, a `/submit` command, it is incredibly not
okay if emails are sent or if comments are added to the respective PR on
GitHub. Setting the `GITGITGADGET_DEBUG` variable will now prevent all
such things from happening.

Signed-off-by: Johannes Schindelin <[email protected]>
These files were generated via:

  npm ci && npm run build && npm run build-dist

Note that recent `ncc` also generates tons of `.d.ts` files and also a
`package.json` file. We don't need the `.d.ts` files (and `.gitignore`
already helpfully matches those), but we do need a minimal
`package.json` file that declares this an ES module, and we do need the
`WELCOME.md` resource, therefore this was staged using:

	echo '{"type":"module"}' >dist/package.json
	cp -R res dist/
	git add -A dist/

Signed-off-by: Johannes Schindelin <[email protected]>
Signed-off-by: Johannes Schindelin <[email protected]>
For easier debugging via `CIHelper.git(...)`.

Signed-off-by: Johannes Schindelin <[email protected]>
Signed-off-by: Johannes Schindelin <[email protected]>
@dscho
Copy link
Member Author

dscho commented Aug 16, 2025

@webstech 🎉 this workflow run just successfully handled this /preview command to send the preview email to me.

It's slowly all coming together!

@dscho
Copy link
Member Author

dscho commented Aug 18, 2025

@webstech while talking about this PR to @mjcheetham, he brought up the question whether this needs to be a set of Actions or whether it would be better to have a single Action covering all the functionality. I can see pros and cons to both approaches. Just to illustrate the difference:

The approach currently chosen in this PR

+    - uses: gitgitgadget/gitgitgadget/handle-pr-comment@v1
+      with:
+        gitgitgadget-git-access-token: ${{ steps.gitgitgadget-git-token.outputs.token }}
+        git-git-access-token: ${{ steps.git-git-token.outputs.token }}
+        dscho-git-access-token: ${{ steps.dscho-git-token.outputs.token }}
+        smtp-options: '{"user": "[email protected]", "host": "smtp.gmail.com", "pass": ${{ toJSON(secrets.GITGITGADGET_SMTP_PASS) }}}'
+        pr-comment-url: ${{ inputs.pr-comment-url }}

The biggest pro I see here is that it allows for defining exactly which inputs are required for any given functionality (update-mail-to-commit-notes does not require a git-git-access-token, for example).

With a single Action

+    - uses: gitgitgadget/gitgitgadget@v1
+      with:
+        gitgitgadget-git-access-token: ${{ steps.gitgitgadget-git-token.outputs.token }}
+        git-git-access-token: ${{ steps.git-git-token.outputs.token }}
+        dscho-git-access-token: ${{ steps.dscho-git-token.outputs.token }}
+        smtp-options: '{"user": "[email protected]", "host": "smtp.gmail.com", "pass": ${{ toJSON(secrets.GITGITGADGET_SMTP_PASS) }}}'
+        command: handle-pr-comment ${{ inputs.pr-comment-url }}

The biggest pro I see here that we will need only one action.yml/index.js pair and therefore can even make the CIHelper class the GitHub Action (by detecting whether this is the main entry point using if (import.meta.url.endsWith(process.argv[1]) CIHelper.main().catch((e) => { console.error(e); process.exitCode = 1; }).

@webstech what is your opinion?

dscho added a commit to gitgitgadget-workflows/gitgitgadget-workflows that referenced this pull request Aug 18, 2025
This is a continuation of the work done in
gitgitgadget/gitgitgadget#1392.

Signed-off-by: Johannes Schindelin <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants