Skip to content
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

Keep PR GitHub metadata up-to-date #25

Merged
merged 148 commits into from Mar 10, 2019

Conversation

eduard-bagdasaryan
Copy link
Contributor

@eduard-bagdasaryan eduard-bagdasaryan commented Jun 11, 2018

When reacting to an event, advance each open PR as much as possible instead of
waiting for the being-merged PR to fully merge before advancing the next PR.
At the minimum, update PR labels.

Keeping all PRs up-to-date required many architectural and implementation
fixes. Most of the "middle" code (somewhere between highest-level GitHub event
processing code and low-level GitHub operations) was rewritten.

Also remove stale PR tags of closed PRs.

Also fixed GH.getTags() which unexpectedly threw when getting an
empty tag list from GitHub.

With this change, before selecting a PR for processing,
the bot updates PR metadata (such as labels, approvals, etc.)
for every PR, except the current one.

Updating the current PR was left as a TODO, because:
* is not so important, since the PR has been adjusted already before
  initiation (i.e., it would not have been initiated without acceptable
  PR metadata).
* it is not obvious how to implement (e.g., add another updater class?)
  because the PrUpdater class does not fit for this
* the current PR would be most likely processed the first, and required
  adjustments would be performed anyway
Updater, Initiator and Finalizer classes were removed in
favor of a new Merger class. This change is untested yet.
* Transformed MergeContext and Merger classes into a single PrProcessor.

* Separated PR updating code from the general processing code, making
  possible to call PrProcessor.update() for all PRs (addressing 85dacc3
  TODO).

* Reworked PrProcessor.process() to return StepResult object (instead of
  inconvenient integer).

Also fixed two bugs:

* setTimeout() does not work properly for ms>2^32-1 (~24 days): in this
  case the value becomes 1ms (and the bot reruns again and again).
  Limit the maximum timeout value.

* Always trim GitHub commit message(used in staged commits) before any
  usage(e.g., comparing) because by default GitHub does that trimming
  itself, when creating the commit.
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a couple of places that need further refactoring work. The rest is polishing.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/PrMerger.js Outdated
let pr = new PullRequest(rawPr);
const result = await pr.process(merging);
if (!merging)
merging = result.succeeded();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works, but I think the following form is more intuitive:

merging = merging || result.succeeded();

There is nothing intuitive about the succeeded() part in this context, but that will be changed to suspended() if other change requests are addressed. Suspended() makes more sense here, especially if we replace the too-specific merging with a general suspendedEarlier:

suspendedEarlier = suspendedEarlier || result.suspended();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Preconditions

// Check 'staging tag' state as merge condition.
// Returns true if there is a fresh merge commit with 'failure' status.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/merge commit with 'failure' status/staged commit with failed status checks/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Fail();
}

if (await this._stagingFailed()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a precondition AFAICT -- staged PRs are handled after they are staged (naturally).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this method into _previoiusStagingFailed() to better reflect its purpose: this check is used to avoid looping over failed (but unchanged) PRs.

this._log("checkApproval: " + this._approval);
await this._setApprovalStatus(this._prHeadSha());
await this._setApprovalStatus(this._tagSha);

if (this._prInProgress()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some conditions, like this one (if it is interpreted without peeking at its current implementation) and _prOpen() are both pre- and post-conditions. Have you tried grouping them into a dedicated _checkTimelessConditions() method that the other two _check*conditions() methods would call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should call _prInProgress() only after commits comparing so that we could track already merged cases (bot-unaware merges). Otherwise, the PR would be considered as 'failed', and restarts from scratch (though it should do only some cleanup and be closed then). So, for 'post' case we should not group _prOpen() and prInProgress().
We can group prOpen() and hasLabel('M-Merged') though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the parts of your your response talking about _prInProgress() -- AFAICT that method checks the WIP: prefix. Checking that PR title prefix has nothing to do with tracking already merged PRs. While it is currently recorded as a part of the title, the prefix should be treated like a label. I suggest s/InProgress/WorkInProgress/ or even s/prInProgress/wipPr/ to reduce clashes between "PR merging progress" and "work-in-progress PR".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest s/InProgress/WorkInProgress/ or even s/prInProgress/wipPr/

Done.

}

async _createStaged() {
this._log("start merging...");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/merging/staging/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

With this change, two outcomes are meaningful for the caller:
* suspended: the PR is in progress so the caller should not
  start a next PR
* delayed: the PR is not ready but should be scheduled

Other outcomes (succeeded() or failed()) mean only that this
PR is not processed anymore (allowing to start another PR).
Copy link
Contributor Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, b853ec7 is the last.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
// Preconditions

// Check 'staging tag' state as merge condition.
// Returns true if there is a fresh merge commit with 'failure' status.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Fail();
}

if (await this._stagingFailed()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed this method into _previoiusStagingFailed() to better reflect its purpose: this check is used to avoid looping over failed (but unchanged) PRs.

src/PrMerger.js Outdated
@@ -24,28 +22,32 @@ class PrMerger {
return labels.find(lbl => lbl.name === Config.clearedForMergeLabel()) !== undefined;
}

async _getPRList(finalizer) {
/// Obtain PR list from GitHub, updates GitHub PR state
/// (labels, approvals, etc.) and filters out PRs not ready
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/PrMerger.js Outdated
@@ -24,28 +22,32 @@ class PrMerger {
return labels.find(lbl => lbl.name === Config.clearedForMergeLabel()) !== undefined;
}

async _getPRList(finalizer) {
/// Obtain PR list from GitHub, updates GitHub PR state
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (running)
return result.delayed() ? result : StepResult.Fail();

if (await this._isStaging()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/PrMerger.js Outdated
let pr = new PullRequest(rawPr);
const result = await pr.process(merging);
if (!merging)
merging = result.succeeded();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

} // MergeFinalizer

// StepResult.Succeed: this PR is in-progress
// StepResult.Delay: this PR is delayed
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a full review, but I added several comments that may lead to some code restructuring and a few polishing suggestions.


await GH.addLabels(params);
if (await this._previousStagingFailed()) {
this._logFailedCondition("fresh merge commit with failed staging checks");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter states the current/true condition. Most other callers state the expected condition (that failed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote into "lack of fresh staging commit with failed checks".

return StepResult.Fail();

if (!this._messageValid) {
this._logFailedCondition("commit message");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say "valid commit message" to clarify that this parameter should document the expected condition (that failed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (currentLabels.find(lbl => lbl.name === label) !== undefined) {
this._log("addLabel: skip already existing " + label);
return;
// checks whether this PR can be tagged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think this description matches the code. Satisfying these conditions allows the bot to tag the PR, but that is not why the bot is checking these conditions. AFAICT, these are preconditions for staging, not just tagging. If tagging was the desired outcome, then the method would have skipped already tagged PRs. How about something like this: whether the PR should be staged (including re-staged)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Fail();

if (this._prInProgress()) {
this._logFailedCondition("not in progress");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/in progress/work-in-progress/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -952,10 +1007,49 @@ class MergeFinalizer extends MergeContext {
return await this._cleanupMerged();

}
} // MergeFinalizer

// StepResult.Suspend: this PR is in-progress
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid "in-progress" term that means little and clashes with "work-in-progress" state. I doubt we need to (re)document StepResult states here. They should be (and probably are) documented where StepResult is declared. Here, we can use just them.

I would describe this method as

// Maintain Anubis-controlled PR metadata.
// If possible, also merge or advance the PR towards merging.
// The caller is responsible for setting PR labels computed here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (await this._hasLabel(Config.mergedLabel(), this.prNumber())) {
this._logFailedCondition("already has merged status");
if (!(await this._isStaging())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub does not let me mark that (unchanged) line below (number 880/888) but the "tagIsFresh()" condition seems out of place here. AFAICT, it should be a part of the "the PR is staged" check that is used as a guard for the staging and merging actions. Fixing this would allow to correctly re-stage stale PRs (during the staging action) instead of simply failing them here (during the merging action).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though moving tagIsFresh() into isStaging() makes this 're-staging' improvement, as you noted, this can be dangerous for some (probably rare) scenarios when PR is 'half-merged'. For example, a PR can be merged to base (mergeToBase() completed successfully), but some of label cleanup failed (e.g. labelMerged()). In this case, the PR is still 'open', but already merged. After that, e.g., either base or PR branch changes so that the tag is not 'fresh' anymore (because the GitHub-generated merge commit changed). If we do as you suggest, the bot on its next iteration would start the PR from scratch. Without these changes, the bot does what is expected - it finishes the cleanup process because it comparers commits first and only after that calls tagIsFresh().

return;
}
throw e;
async _checkTimelessConditions() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Describe this check scope so that we know which checks belong here. I suspect the description would be similar to checks conditions shared by the staging and merging actions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

this._log("checkApproval: " + this._approval);
await this._setApprovalStatus(this._prHeadSha());
await this._setApprovalStatus(this._tagSha);

if (this._prInProgress()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the parts of your your response talking about _prInProgress() -- AFAICT that method checks the WIP: prefix. Checking that PR title prefix has nothing to do with tracking already merged PRs. While it is currently recorded as a part of the title, the prefix should be treated like a label. I suggest s/InProgress/WorkInProgress/ or even s/prInProgress/wipPr/ to reduce clashes between "PR merging progress" and "work-in-progress PR".

@@ -938,9 +941,61 @@ class MergeFinalizer extends MergeContext {
}
}

// Start processing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since "processing" can be interpreted in many ways (metadata updates, staging, merging, staging+merging), I suggest removing these "Start processing" and "Finish processing" labels. Also, grouping methods together like this rarely works long-term. Sooner or later, some of the "start processing" methods will be added elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/RepoMerger.js Show resolved Hide resolved
Copy link
Contributor Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done at d19ba62.

this._log("checkApproval: " + this._approval);
await this._setApprovalStatus(this._prHeadSha());
await this._setApprovalStatus(this._tagSha);

if (this._prInProgress()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest s/InProgress/WorkInProgress/ or even s/prInProgress/wipPr/

Done.

@@ -938,9 +941,61 @@ class MergeFinalizer extends MergeContext {
}
}

// Start processing
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -952,10 +1007,49 @@ class MergeFinalizer extends MergeContext {
return await this._cleanupMerged();

}
} // MergeFinalizer

// StepResult.Suspend: this PR is in-progress
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/RepoMerger.js Show resolved Hide resolved
return;
}
throw e;
async _checkTimelessConditions() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


await GH.addLabels(params);
if (await this._previousStagingFailed()) {
this._logFailedCondition("fresh merge commit with failed staging checks");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewrote into "lack of fresh staging commit with failed checks".

return StepResult.Fail();

if (!this._messageValid) {
this._logFailedCondition("commit message");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -855,25 +862,26 @@ class MergeFinalizer extends MergeContext {
return false; // no staging-only mode by default
}

async _checkConditions() {
const pr = await GH.getPR(this.prNumber(), true);
// checks whether this tagged PR can be merged
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (await this._hasLabel(Config.mergedLabel(), this.prNumber())) {
this._logFailedCondition("already has merged status");
if (!(await this._isStaging())) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this stale check.


if (await this._hasLabel(Config.mergedLabel(), this.prNumber())) {
this._logFailedCondition("already has merged status");
if (!(await this._isStaging())) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though moving tagIsFresh() into isStaging() makes this 're-staging' improvement, as you noted, this can be dangerous for some (probably rare) scenarios when PR is 'half-merged'. For example, a PR can be merged to base (mergeToBase() completed successfully), but some of label cleanup failed (e.g. labelMerged()). In this case, the PR is still 'open', but already merged. After that, e.g., either base or PR branch changes so that the tag is not 'fresh' anymore (because the GitHub-generated merge commit changed). If we do as you suggest, the bot on its next iteration would start the PR from scratch. Without these changes, the bot does what is expected - it finishes the cleanup process because it comparers commits first and only after that calls tagIsFresh().

The major change here is a new 'post-staged' processing state (in
addition to 'pre-staged' and 'staged'), which starts just after
successful fast-forwarding into the base branch.  During this step all
required PR cleanup should be done atomically. The difference between
first two state and this new one is in that how errors/exceptions are
handled.  If there is an exception during pre-staged/staged, the
higher-level code (PrMerger.js) just starts another PR.  For post-staged
exceptions, the PR is considered as 'suspended', and does not start
another PR, until all required cleanup is completed.

The PR state (and related information) is now calculated just after
PR code gets control. This allowed to eliminate confusion and
code duplication (e.g., with _tagIsFresh()).

A special care is taken over possible 'half-cleanupped' PRs, when an
exception occurs during post-staged. In this case, the code should aware
that the requested PR list does not contain this PR and 'mergeable'
status is not calculated for this PR.
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one or two potentially important questions (especially the one about suspending a PR that fails cleanup). The rest is polishing.

src/MergeContext.js Outdated Show resolved Hide resolved
await this._applyLabels();
throw e;
}
if (!this._prState.postStaged())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not apply labels unconditionally here? In other words, why is label application in post-staged state requires some special handling/protection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done for cleanup atomicity purpose, as I described above. If we apply labels unconditionally here and an exception occurs while applying, this PR would be abandoned and cleanup would not finish.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me yet why an exception during applyLabels() would result in PR abandonment. Essentially the same concern is discussed inside _finalize() in my new review, so let's resolve that concern first. I am adding this comment (in an older review) just to remember to come back to this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
await this._loadPrState();
this._log("PR state calculated: " + this._prState.toString());
let result = await this.update();
if (anotherPrWasStaged)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assert that update() never "suspends" the PR (i.e., never prevents another PR from being merged)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Show resolved Hide resolved
src/PrMerger.js Outdated
assert(result.succeeded());
}
return false;
}

// Loads 'being-in-merge' PR, if exists (the PR has tag and staging_branch points to the tag).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would clarify the intent along these lines:

// returns raw PR with a commit at the tip of the staging branch (or null)
// if that PR exists, it is either "staged" or "post-staged"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/MergeContext.js Show resolved Hide resolved
Copy link
Contributor Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done at bfb516f.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
this._log("PR state calculated: " + this._prState.toString());
let result = await this.update();
if (anotherPrWasStaged)
return result.delayed() ? result : StepResult.Fail();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could return 'success' here as well: the caller does not care about these outcomes. However, I was thinking that this method returns 'success' if it managed to start/finish this PR and 'fail' in other cases (other from delaying). For example, it returns 'fail' if a PR condition failed. Similarly, we assume that 'anotherPrWasStaged' is a condition preventing the PR from start and thus should return 'fail'.

src/MergeContext.js Show resolved Hide resolved
await this._applyLabels();
throw e;
}
if (!this._prState.postStaged())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done for cleanup atomicity purpose, as I described above. If we apply labels unconditionally here and an exception occurs while applying, this PR would be abandoned and cleanup would not finish.

src/PrMerger.js Outdated
assert(result.succeeded());
}
return false;
}

// Loads 'being-in-merge' PR, if exists (the PR has tag and staging_branch points to the tag).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/MergeContext.js Outdated Show resolved Hide resolved
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You responses to the previous review were useful, but one or two related/overlapping concerns remain since the last review. The rest is minor polishing.

@@ -4,7 +4,9 @@ const Log = require('./Logger.js');
const GH = require('./GitHubUtil.js');
const Util = require('./Util.js');

// A result produced by MergeInitiator and MergeFinalizer classes
// Action outcome, with support for paused and snoozed actions.
// For example, is returned by PullRequest.process() (at higher level) or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// For example, is returned by PullRequest.process() (at higher level) or
// For example, returned by PullRequest.process() (at higher level) or

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

await this._labels.apply();
}

// Atomize PR post-merge cleanup: suspend this PR
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description does not seem to match the code or is misleading -- we are not suspending a PR until its cleanup is complete; we suspend only after cleanup fails. The end result should be the same, but the sequence (and associated risks) are rather different.

The problem, in part, lies in the fact that we do not really suspend a PR here -- we only return the corresponding StepResult, but the PR state itself remains unchanged.

Another problem is that to make an operation (e.g., cleanup) "atomic" means that any partial result is undone during failures -- the operation either succeeds as a whole or has no side effects. This is not what happens here -- we undo nothing. This method does not make PR cleanup atomic.

I suggest the following description:

// Cleans up and closes a post-staged PR, removing it from our radar for good.
// Returns "PR has been suspended" result on failures.

Adjust as needed based on the other change requests, of course.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Succeed();
} catch (e) {
Log.LogException(e, this._toString() + " unexpected error while finalizing.");
return StepResult.Suspend();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an earlier review:

Alex: Why suspend a PR that fails cleanup?

Eduard: The purpose is to guarantee PR consistency on GitHub. If the bot leaves a half-cleanupped PR on GitHub (and takes another one), it would not be able to fix this PR in future, because of its closed/merged attributes. So, if during cleanup there is an accidental GitHub-related error (which happen periodically), we just restart and finish cleanup as usual (after timeout). In case of a more serious bug, causing this exception, the bot should not pass to another PR either: it is better to do nothing in this case, until the bug is fixed.

I believe I understand what you are saying, but I am not sure I agree that special handling is actually desirable here. I may be missing some important detail about PR state. What are the flaws/mistakes in the following logic?

_finalize() deals with open post-staged tagged PRs. There are three cleanup steps:

  1. If _applyLabels() fails, then the PR state does not change: The PR remains open, post-staged, and tagged. Thus, we can let _applyLables() throw and let the higher level code deal with that error as any other PR update failure (i.e., by eventually restarting the PR update process which will bring us back to the same _finalize() call). No special treatment seems to be needed to handle this step failure.
  2. If closing the PR fails, then the PR state does not change: The PR remains open, post-staged, and tagged. Thus, we can let updatePR() throw and let the higher level code deal with that... (see step 1 for details).
  3. If deleteReference() fails, then the PR is going to be closed/ignored but the tag will remain in the repository. I suspect that we should delete all such stale tags as a special high-level repository maintenance step (not dedicated to any specific PR). In fact, it may be easier to delete all stale tags in one location rather than delete them both here (under normal conditions; here they have been "stale" for just a moment since the previous updatePR() call) and then check for all truly stale tags in some higher-level code. We can just do the latter and, by removing this step, remove the need for special treatment from this code.

In summary, the first two update steps do not seem to need special handling while the third step should probably be moved to a central location (where it will stop being a "special" step). Am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

await this._applyLabels();
throw e;
}
if (!this._prState.postStaged())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me yet why an exception during applyLabels() would result in PR abandonment. Essentially the same concern is discussed inside _finalize() in my new review, so let's resolve that concern first. I am adding this comment (in an older review) just to remember to come back to this later.

let result = await this.update();
if (anotherPrWasStaged) {
assert(!result.suspended());
return result.delayed() ? result : StepResult.Fail();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please always return result here. This was discussed in an earlier review, and I understand that you were trying to reserve success for PRs that were fully merged, but I do not think that translation is used/useful in the current code (AFAICT you agree with that), and it requires extra code (that confuses readers like me and would require a comment to define _doProcess() result).

Let's simplify by always returning result until the caller starts caring about the outcome (other than "suspended PR"). If that ever happens, we would know what the caller new needs are and will adjust this code accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (this._prState.postStaged()) {
this._role = "finalizer";
this._labelMerged();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This _labelMerged() operation should be a part of _finalize() method because that method is where we process postStaged() PRs. With other changes discussed in this review, it is possible that _finalize() will be called here, just like any other PR processing step, but even if _finalize() has to be called later/specially, I think this _labelMerged() call would belong there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -96,6 +96,10 @@ class RepoMerger {

_plan(ms) {
assert(ms > 0);
// obey node.js setTimeout() limits
const maxMs = Math.pow(2, 31) - 1;
ms = ms < maxMs ? ms : maxMs;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ms = ms < maxMs ? ms : maxMs;
const ms = Math.min(requestedMs, maxMs);

and rename the parameter to requestedMs, of course.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

// manage pull request states, one of:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// manage pull request states, one of:
// A state of a pull request (with regard to merging progress). One of:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Before this change, an error during PR finalizing (tagging, closing the
PR or removing the PR tag) caused the PR suspending.  At the
next iteration this PR would become the current one, repeating all these
steps (until all of them succeed); other PRs still wait.

We gain two advantages with this improvement:

* Error handling unification: an error at PR finalization is treated
  as errors at other steps (pre-staging or staging).

* Better performance: instead of repeating finalizing the failed PR
  (which may fail again and again) we start the next PR as usual. The
  PR finalization will be completed once it becomes the current again.
  Finalization for closed (but still tagged) PRs is done specially, in
  a central location, where tags for all such PRs are deleted.
* Made _finalize() an explicit PR processing step call within
  doProcess(), like _stage() and _mergeStaged().

* Polished method descriptions and variable names.
* Do PR finalization while 'updating' PRs. This should help
  to cleanup PR finalization failures even if another PR is
  being staged.

* Honor 'dry run' in _cleanTags().

* Optimize by filtering off PR-unrelated tags in _cleanTags().

* TagRegex should exactly match 'M-staged-PR...' name.

* Methods description.
Copy link
Contributor Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the last change is a40e11.

@@ -4,7 +4,9 @@ const Log = require('./Logger.js');
const GH = require('./GitHubUtil.js');
const Util = require('./Util.js');

// A result produced by MergeInitiator and MergeFinalizer classes
// Action outcome, with support for paused and snoozed actions.
// For example, is returned by PullRequest.process() (at higher level) or
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

// manage pull request states, one of:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

await this._labels.apply();
}

// Atomize PR post-merge cleanup: suspend this PR
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Succeed();
} catch (e) {
Log.LogException(e, this._toString() + " unexpected error while finalizing.");
return StepResult.Suspend();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

let result = await this.update();
if (anotherPrWasStaged) {
assert(!result.suspended());
return result.delayed() ? result : StepResult.Fail();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


if (this._prState.postStaged()) {
this._role = "finalizer";
this._labelMerged();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

await this._applyLabels();
throw e;
}
if (!this._prState.postStaged())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -96,6 +96,10 @@ class RepoMerger {

_plan(ms) {
assert(ms > 0);
// obey node.js setTimeout() limits
const maxMs = Math.pow(2, 31) - 1;
ms = ms < maxMs ? ms : maxMs;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

This fixes c633ff: we should finalize during update only non-current
PRs, since the current PR is finalized at the post-staged step, as
usual.
... by moving its stage-specific code into the corresponding ifs in
doProcess().
It was addressed at 38574c.
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two places that may need non-trivial changes (look for two change requests one tied to _stagedCommitWillFail and one talking about the_tagIsFresh() description). The rest is trivial fixes/polishing.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/PrMerger.js Outdated Show resolved Hide resolved
This is needed for branches conflicts detection: the staged commit
is obviously stale if there are conflicts (i.e., there were changes
which caused these conflicts).

Also some polishing and methods renaming.
... and set the label. Otherwise, the developer may miss this important
information about broken PR code/tests.
... thus avoiding unnecessary explicit setting this field.
Brewing PRs have nothing to resume on errors: such PRs are
started from 'scratch' on any error.
Copy link
Contributor Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c819b-13d581.

src/PrMerger.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
}
}

// promises to process a single PR at once, hiding PullRequest from callers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// promises to process a single PR at once, hiding PullRequest from callers
// promises to update/advance the given PR, hiding PullRequest from callers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (!this._prMergeable()) {
this._logFailedCondition("mergeable");
return StepResult.Fail();
// The staged commit became out of sync with PR and(or) base branches.
Copy link
Contributor

@rousskov rousskov Feb 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this comment -- the log message below it should be enough to [self-]document this code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Fail();
// The staged commit became out of sync with PR and(or) base branches.
if (!(await this._stagedCommitIsFresh())) {
this._log("the staged commit became stale due to PR branch and(or) base branch changes");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log message became stale. I do not think we should enumerate all _stagedCommitIsFresh() checks here:

Suggested change
this._log("the staged commit became stale due to PR branch and(or) base branch changes");
this._log("the staged commit is stale");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/MergeContext.js Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for addressing all my change requests. I think we are getting close to being done here, but should implement at least one of the TODOs you have added (see "commit message generation check") and discuss the other related TODO. The rest is minor polishing.

src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
Copy link
Contributor Author

@eduard-bagdasaryan eduard-bagdasaryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done at 64f051.

src/MergeContext.js Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
src/MergeContext.js Outdated Show resolved Hide resolved
if (!this._prMergeable()) {
this._logFailedCondition("mergeable");
return StepResult.Fail();
// The staged commit became out of sync with PR and(or) base branches.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return StepResult.Fail();
// The staged commit became out of sync with PR and(or) base branches.
if (!(await this._stagedCommitIsFresh())) {
this._log("the staged commit became stale due to PR branch and(or) base branch changes");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}

// promises to process a single PR at once, hiding PullRequest from callers
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

For example, we should not add "M-failed-staging-checks" if someone
requested changes.
This is still a temporary solution: a complete solution should
properly distinguish 'superficial' checks such as approvals from
genuine checks, reported by CIs. Left that as a TODO.
Staged statues, loaded multiple times during a PR processing cycle may
differ. We should avoid possible inconsistency when code decisions are
based on different sets of staged statues.
The first(incomplete) step was done in a542cd for staged commit
statuses. The similar logic is applicable to PR statuses. Since the
statuses are loaded only once now, we need carefully maintain
StatusChecks container after creating new approval statuses or copying
PR statuses into staged commit statuses.
3b3f9e had a bug: we need to overwrite approval status instead
of just copy it into the array.
Copy link
Contributor

@rousskov rousskov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot really track all the intricate dependencies in these latest changes, but I did not find any bugs. I inlined one polishing request. Please proceed with merging this when you are happy with the code.


if (approvalStatus && this._approval.matchesGitHubStatusCheck(approvalStatus)) {
this._log("approval status already exists: " + Config.approvalContext() + ", " + this._approval);
if (this._dryRun("setting approval status"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this check should be moved into _createApprovalStatus() because hasApprovalStatus() and addApprovalStatus() calls do not modify GitHub (and do allow us to get deeper in dry-run mode).

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.

None yet

2 participants