Add support for distributed notes updates - #1473
Conversation
eae63d0 to
97f2c64
Compare
30999e3 to
ba10771
Compare
ba10771 to
88309db
Compare
77d6665 to
d88727f
Compare
|
@webstech could you please have a look? |
|
Still reviewing (re-reviewing really). The The |
Thank you!
I would like to have a nicer user experience, though, especially in the (hopefully not so far) future when I can use GitGitGadget also to contribute to Cygwin (i.e. run this Action in a fork of That means that I'd like to reduce the number of steps anyone has to go through to set up their own "GitGitGadget instance", which means: no more |
|
|
||
| if (optionsUpdated) { | ||
| await this.notes.set("", options, true); | ||
| await this.notes.push(this.urlRepo); |
There was a problem hiding this comment.
CIHelper::handlePR is doing a push. Is any of this necessary now?
There was a problem hiding this comment.
Good point! I'll drop this call.
|
|
||
| if (optionsUpdated && updateOptionsInRef) { | ||
| await this.notes.set("", options, true); | ||
| await this.notes.push(this.urlRepo); |
There was a problem hiding this comment.
Is updateOptionsInRef still useful? It seems outdated and would allow the update if needed which would simplify upstream callers. optionsUpdated does not need to return true if the notes are actually updated but that only matters if callers use it.
There was a problem hiding this comment.
It is totally outdated, thank you for pointing that out! I will drop it in a new commit.
Maybe the options get needs to be able to push updates if it does not have a full object and callers can assume they have a complete object. Having checks for missing fields is noise and complicates code. Some code is deciding if the options is updated and then calling code that also has to decide if the options is updated. Now that code (ie handlePR) is pushing the update but the caller is also thinking it may need to do an update. Edit: |
That's probably the cleanest solution. A more convenient solution would be to obtain the "collaborator permission" (similar to what Git for Windows' GitHub App does), so that automatically all accounts with write permission would be added. Instead of resorting to GraphQL, this could be done via the $ gh api repos/gitgitgadget/git/collaborators --jq \
'.[] | select(.permissions.admin or .permissions.maintain or .permissions.push) | .login' |
I guess the following would be enough: $ gh api repos/gitgitgadget/git/collaborators --jq '.[] | select(.permissions.push) | .login' |
webstech
left a comment
There was a problem hiding this comment.
I have finished reviewing the other commits with no additional comments. Thanks for the education.
I will re-review when your changes arrive.
d88727f to
10df503
Compare
Thank you! And my apologies, I had forgotten to push my updates... 🤦 |
| } | ||
|
|
||
| logger.log("Publishing branch and tag"); | ||
| await this.publishBranch(tagName); |
There was a problem hiding this comment.
I did not look at what cleanup could occur but there are quite a few branches and tags on the git repo. Are there more than is currently needed?
There was a problem hiding this comment.
The code under discussion is a left-over from the CLI times, when the branch had not necessarily been published yet. The log message still talks about that branch.
However, what does need to still be pushed, even in these times, is the tagged version that reflects the iteration that has been sent to the mailing list.
That function is already provided by the conditional block if (publishTagsAndNotesToRemote) { ... } below, though, and therefore the publishBranch() logic is no longer needed.
The branches that are in gitgitgadget/git are actually synchronized by the sync-* workflows from some upstream sources, and serve only as potential PR target branches.
The vast majority of the tags in gitgitgadget/git are the ones reflecting submitted patch series iterations, the remainder have been mirrored by the sync-* workflows together with the branches.
|
|
||
| await expect(ci.handleComment("gitgitgadget", 433865360)).rejects.toThrow(/is not a valid GitHub username/); | ||
| expect(ci.addPRCommentCalls[0][1]).toMatch(/is not a valid GitHub username/); | ||
| await expect(ci.handleComment("gitgitgadget", 433865360)).rejects.toThrow(/SOME ERROR/); |
There was a problem hiding this comment.
The purpose of the test is to verify the bad user is reported by CIHelper. Verifying the network access was bypassed seems like a different test. In the CI test, github throws a bad request type error that CIHelper converts to the bad user with the GitHubGlue error message as part of the error. Running locally, the test actually gets a not authenticated error thrown by GitHubGlue (so not quite the test I thought it was). Setting
getConfig().repo.owner = "gooduser"; fixes the auth error if the config is properly set up. I understand what the update is proofing but don't think it is part of the test - the checks do not need to be changed.
There was a problem hiding this comment.
Wait, you mean to say that the patch avoiding network access to GitHub REST should be dropped?
There was a problem hiding this comment.
Running locally, there is no network access due to no authentication (required to ask about users). Running on workflows or pipelines, there is network access but is that a problem? I'll let you decide on that.
I think, either way, the tests should still be checking for is not a valid GitHub username and not SOME ERROR since that is the purpose of the test. CIHelper is setting the not valid message. Sorry I was not clear.
@dscho Did you see this?
|
@dscho Thanks for the updates. Sorry but a couple of minor things. fyi, due to the change overlap, I reviewed the complete change instead of individual commits - too much to track when comparing with local source. I also assumed large commits I had not commented on were unchanged. |
10df503 to
836f3db
Compare
In aefc723 (correspond draft PR and 'RFC PATCH', 2025-01-16) we introduced code that my local linter claims is an unsafe assignment. Let's work around that. Signed-off-by: Johannes Schindelin <[email protected]>
To handle the situation when concurrent GitGitGadget operations want to modify the state that is persisted in Git notes (such as `refs/notes/gitgitgadget`), we need a way to reconcile diverging Git notes. Happily, the way GitGitGadget works makes this relatively straight-forward, as concurrent operations usually do not conflict and merely need to add/modify/remove independent attributes and/or values. This allows us to treat these Git notes as kind of a Conflict-free Replicated Data Type (https://en.wikipedia.org/wiki/CRDT). The strategy implemented in this patch is relatively simple, and follows the "local-first" idea (where state is applied locally first and only then persisted remotely): The newly added method determines what changes were made locally, and then reapplies them on top of the upstream note commit. The local operations need to fall into one of the following categories: - a note has been appended to - a single line of text (which may, or may not, be a stringified JSON object) is added as a note on an object that had not had a note yet - a note containing a single line of text has been replaced with a new, single line of text - a note that contained a JSON object (stringified as a single line of text) has been modified. Valid modifications include: - an attribute that was added - an attribute that has been removed - an attribute containing a primitive type (string, number or boolean) has been modified (if local and upstream diverged, local wins) - items from an attribute containing an array have been removed and/or added - an attribute containing a Plain Old Javascript Object has seen any of these modifications Other modifications are as yet unhandled. For historical reasons (*cough* Git commands implemented in Unix shell scripts *cough*) it is impossible to add a Git note that does not end in a newline character. Let's ensure that we don't do that, either. To avoid tampering with a Git index, let's use a temporary index. This requires the environment variable `GIT_INDEX_FILE` to be passed to the Git processes, via the `env` attribute of the `IGitOptions` (which only need to be declared and no other changes are needed because the `options` are then used as dugite's `IGitExecutionOptions`, which already has the `env` attribute). Signed-off-by: Johannes Schindelin <[email protected]>
We actually do want to work exclusively on bare repositories. A recent Git version requires either the `safe.bareRepository` config setting to indicate that it is safe, or the git dir to be specified explicitly via `--git-dir`. Let's do the latter. This will become very important when we switch to using GitHub Actions with ephemeral (and partial) clones of `gitgitgadget/git`. Signed-off-by: Johannes Schindelin <[email protected]>
In the olden days, when GitGitGadget was still `mail-patch-series.sh`, the source of truth was the local repository where it was run, and as such, the branch needed to be pushed to the remote to make it, well, public. However, in the current setup, the source of truth is the gitgitgadget/git (or git/git) repository's Pull Request, i.e. the branch is already published. Therefore, this branch publishing logic should have been removed in 6d397b0 (Remove support for the CLI mode, 2023-12-31)... better late than never... Signed-off-by: Johannes Schindelin <[email protected]>
Let's only ever push `refs/notes/gitgitgadget` from `GitGitGadget.pushNotesRef()`. This will make the next change easier, where we make notes pushing more resilient against concurrency by incorporating the shiny new `GitNotes.notesSync()` call. Signed-off-by: Johannes Schindelin <[email protected]>
GitGitGadget's state lives in a JSON object at the tip of the `refs/notes/gitgitgadget` ref at https://github.com/gitgitgadget/git. When GitGitGadget's automation runs, it typically modifies that, and then pushes the new state. In case of concurrent runs, this would cause a problem (which is why we currently prevent that). But not for long! This here commit uses the `notesSync()` function that was introduced _just_ for the purpose of allowing concurrent runs, by merging whatever changed upstream and then pushing again. To allow for multiple concurrent contenders, let's try that a couple of times before giving up, waiting for an increasingly long time. This functionality is now made available via the shiny new `GitNotes.push(url)` method. This method will henceforth be used by the `GitGitGadget.pushNotesRef()` method to guarantee robustness for concurrent GitGitGadget workflows. Signed-off-by: Johannes Schindelin <[email protected]>
I'd like the following `misc-helper` commands to call the new `GitNotes.push()` method as needed, to benefit from the just-introduced support for concurrent GitGitGadget workflow runs: - update-open-prs - update-commit-mappings - handle-open-prs - handle-new-mails Currently, these are handled in the "Update GitGitGadget's PRs" Pipeline at https://dev.azure.com/gitgitgadget/git/_build/index?definitionId=2&_a=completed and in the "Mirror Git List to GitGitGadget's PRs" Azure Pipeline at https://dev.azure.com/gitgitgadget/git/_build/index?definitionId=5&_a=completed The Pipeline definitions perform those pushes manually, and changing CIHelper to do the push preemptively should allow for those definitions to remain unchanged until the point in time when they are replaced by workflows in https://github.com/gitgitgadget/gitgitgadget-workflows. Technically, this commit does not even touch `misc-helper.ts`; The reason is that the logic is actually implemented in the `CIHelper` class, which is a good thing, because that class will become the principal entry-point of above-mentioned GitHub workflows. Also note that there is a slight change of (`push`) behavior in `handle-open-prs`: This command calls `CIHelper.handleOpenPRs()`, which in turn calls `CIHelper.handlePR()` in a loop. Now, the notes ref will be pushed every time the latter updates something, where before, the notes ref was only pushed once, at the very end. This is arguably safer because PRs that have already been handled in a Pipeline run that fails somewhere in the middle won't be handled again upon the re-run. Eventually I'd like the same graceful notes ref pushing even for script/update-mail-to-commit-notes.sh (which updates not the `gitgitgadget`, but the `mail-to-commit` and `commit-to-mail` notes refs), so that the "Update GitGitGadget's commit to mail notes" Pipeline at https://dev.azure.com/gitgitgadget/git/_build/index?definitionId=9&_a=completed can also be migrated to a GitHub workflow. Signed-off-by: Johannes Schindelin <[email protected]>
While we may want to consider taking out the dry-run mode (as it is not surfaced in GitGitGadget ever since 6d397b0 (Remove support for the CLI mode, 2023-12-31) removed the ability to run it in command-line mode), the mode still exists and pushing a tag (and now also notes ref) should definitely not be done in dry-run mode. Pointed-out-by: Chris. Webster <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
All current callers pass in a valid `options` parameter, therefore there is no need any longer for the original logic to potentially initialize those options. Signed-off-by: Johannes Schindelin <[email protected]>
The `CIHelper.handlePR()` method already took care of updating the notes, no need to do it again. Signed-off-by: Johannes Schindelin <[email protected]>
… `handlePR()` We already push the notes ref when updating the PR metadata (i.e. the object attached to the blob whose contents are the PR URL), therefore it makes only sense to combine that with any update to the GitGitGadgetOptions (i.e. the object attached to the empty object). This simplifies the logic (and the separation of concerns) quite a bit. Suggested-by: Chris. Webster <[email protected]> Signed-off-by: Johannes Schindelin <[email protected]>
836f3db to
d011ae5
Compare
Okay, I still don't know, but I really want to merge this PR, so I simply dropped that patch from the PR and will merge as soon as CI finishes successfully. |
When merging #1473 (Add support for distributed notes updates), I did not anticipate that the current Azure Pipelines were not prepared for that: Contrary to what I had remembered, they did _not_ simply configure `http.extraHeader` to be able to push (which is somewhat surprising because I authored them, and I typically did that a lot in the olden days). Happily, I _just_ introduced support for configuring a token in `CIHelper` that is specifically used for accessing `gitgitgadget/git`, including the Git notes pushes. Let's teach `misc-helper` about this new trick, and pick up the token that was configured via `set-app-token` and tell `CIHelper` to use it. Signed-off-by: Johannes Schindelin <[email protected]>
When merging #1473 (Add support for distributed notes updates), I did not anticipate that the current Azure Pipelines were not prepared for that: Contrary to what I had remembered, they did _not_ simply configure `http.extraHeader` to be able to push (which is somewhat surprising because I authored them, and I typically did that a lot in the olden days). Happily, I _just_ introduced support for configuring a token in `CIHelper` that is specifically used for accessing `gitgitgadget/git`, including the Git notes pushes. Let's teach `misc-helper` about this new trick, and pick up the token that was configured via `set-app-token` and tell `CIHelper` to use it. Signed-off-by: Johannes Schindelin <[email protected]>
One of the biggest blockers preventing GitGitGadget from moving away from Azure Pipelines toward GitHub workflows is the complication that the
refs/notes/gitgitgadgetnote holds global state and must therefore be updated sequentially.In the Azure Pipelines approach, we manage that by having an agent pool of one, which naturally can only serve one request at a time. That way, every job can first fetch the GitGitGadget notes ref, do whatever it needs to do, leisurely update the notes ref to reflect the new state and push the ref. Then, the next job can do the same and there is never a situation where a job tries to push the notes ref and fails because another, concurrent job has updated said ref in the meantime.
For various reasons, adding a pool of one to GitHub Actions is impractical. Neither is it desirable.
In this PR, I propose a solution that allows for jobs to run concurrently, fetching the GitGitGadget notes, doing their thing, updating the notes ref along the way, and once it is time to push the notes ref and a situation is detected where it does not fast-forward, combine the changes in a way that is inspired by Conflict-free replicated data types (CRTD), so that it can be pushed without problems after all.
The strategy implemented in this PR is to prefer the local changes in case of a hard conflict.
This means we will still have to be careful to prevent concurrent runs of some GitHub workflows: for example, we really will only ever want at most a single instance of the job to run whose responsibility it is to read new mails from the Git mailing list and mirror the relevant ones into the corresponding PRs. Should two jobs of that workflow run at the same time, they would end up adding PR comments twice, which is undesirable.
This PR already updates the
misc-helpercommands that are run by GitGitGadget's Azure Pipelines: This is preemptive, so that I can start porting the Pipelines to GitHub workflows and not risk failed notes ref synchronizations.