Capture pull request labels before archiving a review stack - #1501
Open
c-gerke wants to merge 1 commit into
Open
Capture pull request labels before archiving a review stack#1501c-gerke wants to merge 1 commit into
c-gerke wants to merge 1 commit into
Conversation
`LabelCapturingHandler` is the only handler that writes `pull_request.labels` on a `labeled` or `unlabeled` event, and it skips stacks that are already archived. `Webhooks.default_handlers` registers it after `LabeledHandler` and `UnlabeledHandler`, so on the one label event that matters most the capture never runs: 1. The lifecycle handler sees `archive?` and archives the stack. 2. `LabelCapturingHandler` runs next, finds the stack archived, and skips the capture. The persisted list therefore keeps the label that caused the teardown until some later event happens to rewrite it. Both provisioning behaviors are affected, in opposite directions: under `allow_with_label` an archived stack still carries the provisioning label whose removal archived it, and under `prevent_with_label` the blocking label that archived the stack is never stored at all. The column is not only informational. `ReviewStack#env` turns each stored label into a `LABEL_NAME=true` environment variable for the stack. Capture the payload's labels in the lifecycle handlers, immediately before the archive, so an archived stack's labels describe the event that archived it. The capture is deliberately narrow. A stack that is already archived is left alone, keeping its labels a snapshot of the event that archived it rather than a running mirror of the pull request. The unarchive and creation paths are untouched and keep capturing through `LabelCapturingHandler` and `ReviewStackAdapter#create!` as before.
c-gerke
marked this pull request as ready for review
September 9, 2026 16:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
LabelCapturingHandleris the only handler that writespull_request.labelson alabeledorunlabeledevent, and it skips stacks that are already archived —labeled_active_stack?andunlabeled_active_stack?both require!stack.archived?.Webhooks.default_handlersregistersit after
LabeledHandlerandUnlabeledHandler.So on a label event that archives the stack, the capture never runs:
archive?and callsstack.archive!.LabelCapturingHandlerruns next, finds the stack archived, and skips the capture.The persisted list keeps the label that caused the teardown until some later event happens to
rewrite it. Both provisioning behaviors are affected, in opposite directions:
allow_with_labelprevent_with_labelAny other label is unaffected under either behavior: nothing archives, the stack stays active,
and the capture runs normally. The defect is specific to the label named by
provisioning_label_name.The column is not only informational —
ReviewStack#envturns each stored label into aLABEL_NAME=trueenvironment variable for the stack.Approach
Capture the payload's labels in
LabeledHandler#handleandUnlabeledHandler#handle,immediately before the archive, so an archived stack's labels describe the event that archived
it.
LabelCapturingHandlerbefore the lifecycle handlersReviewStackAdapter#archive!ClosedHandlerarchives through the same method, so every pull request close would take on a label write and apull_requestupdate hook.The capture is guarded on the stack being present and not already archived, which keeps an
archived stack's labels a snapshot rather than a running mirror, and keeps repeat deliveries
from rewriting them.
capture_labelsis duplicated across the two handlers, matching howarchive?,unarchive?,stack,repository, andpull_request_label_namesare already duplicated between them. Gladto extract a shared module instead if you would rather have one.
Scope
LabelCapturingHandlerstill captures for a stackthat a label event brings back into service, and
ReviewStackAdapter#create!still stores thepayload's labels on a newly created stack.
update!(labels:)thatLabelCapturingHandler#capture_labelsalready performs on every label event against an activestack, so it carries the same exposure. Rescuing here and nowhere else seemed worse than
matching the existing behavior.
pull_requestupdate hook on the archive path, emitted before the stack isarchived.
EditedHandlerandAssignedHandlercallupdate(github_pull_request:)with no archived check, andPullRequest#github_pull_request=assigns
labels, so aneditedorassignedevent on an archived pull request overwriteswhat was captured. That does not reintroduce this bug — the overwrite writes current labels,
which no longer carry the removed provisioning label — so it is left alone, but the capture
should not be read as permanent.
Verification
Eight cases added to the existing handler tests, four in each file. They use each file's local
create_stack/create_archived_stack/configure_provisioning_behaviorhelpers and theexisting
pull_request_labeled/pull_request_unlabeledpayload fixtures; no new fixtures wereneeded.
unlabeled_handler_test.rb— theallow_with_labelcase, the empty-list case where the lastlabel is removed, no capture when the stack is already archived, no capture when unarchiving,
and one case that dispatches through
Shipit::Webhooks.for_event("pull_request")so the wholeregistered chain runs in its real order rather than one handler in isolation.
labeled_handler_test.rb— theprevent_with_labelcase, no capture when the stack is alreadyarchived, no capture when unarchiving.
Confirmed failing without the fix. Reverting just the two
handlechanges fails 4 of the 8:the
allow_with_labelcase, theprevent_with_labelcase, the empty-list case, and thefull-chain case. The other 4 assert unchanged behavior and stay green either way, which is what
they are there for.
Full suite locally, sqlite: 1253 runs, 3842 assertions, 0 failures, 0 errors. Rubocop clean on
the four changed Ruby files.