diff --git a/Makefile b/Makefile index c45597b4d193..9319945409f6 100644 --- a/Makefile +++ b/Makefile @@ -181,6 +181,12 @@ IMPORT_BOSS_VER := v0.28.1 IMPORT_BOSS := $(abspath $(TOOLS_BIN_DIR)/$(IMPORT_BOSS_BIN)) IMPORT_BOSS_PKG := k8s.io/code-generator/cmd/import-boss +TRIAGE_PARTY_IMAGE_NAME ?= extra/triage-party +TRIAGE_PARTY_CONTROLLER_IMG ?= $(STAGING_REGISTRY)/$(TRIAGE_PARTY_IMAGE_NAME) +TRIAGE_PARTY_DIR := hack/tools/triage +TRIAGE_PARTY_TMP_DIR ?= $(TRIAGE_PARTY_DIR)/triage-party.tmp +TRIAGE_PARTY_VERSION ?= v1.6.0 + CONVERSION_VERIFIER_BIN := conversion-verifier CONVERSION_VERIFIER := $(abspath $(TOOLS_BIN_DIR)/$(CONVERSION_VERIFIER_BIN)) @@ -1482,6 +1488,60 @@ $(GOVULNCHECK): # Build govulncheck. $(IMPORT_BOSS): # Build import-boss GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) $(IMPORT_BOSS_PKG) $(IMPORT_BOSS_BIN) $(IMPORT_BOSS_VER) +## -------------------------------------- +## triage-party +## -------------------------------------- + +.PHONY: release-triage-party +release-triage-party: docker-build-triage-party docker-push-triage-party clean-triage-party + +.PHONY: release-triage-party-local +release-triage-party-local: docker-build-triage-party clean-triage-party ## Release the triage party image for local use only + +.PHONY: checkout-triage-party +checkout-triage-party: + @if [ -z "${TRIAGE_PARTY_VERSION}" ]; then echo "TRIAGE_PARTY_VERSION is not set"; exit 1; fi + @if [ -d "$(TRIAGE_PARTY_TMP_DIR)" ]; then \ + echo "$(TRIAGE_PARTY_TMP_DIR) exists, skipping clone"; \ + else \ + git clone "https://github.com/google/triage-party.git" "$(TRIAGE_PARTY_TMP_DIR)"; \ + cd "$(TRIAGE_PARTY_TMP_DIR)"; \ + git checkout "$(TRIAGE_PARTY_VERSION)"; \ + git apply "$(ROOT_DIR)/$(TRIAGE_PARTY_DIR)/triage-improvements.patch"; \ + fi + @cd "$(ROOT_DIR)/$(TRIAGE_PARTY_TMP_DIR)"; \ + if [ "$$(git describe --tag 2> /dev/null)" != "$(TRIAGE_PARTY_VERSION)" ]; then \ + echo "ERROR: checked out version $$(git describe --tag 2> /dev/null) does not match expected version $(TRIAGE_PARTY_VERSION)"; \ + exit 1; \ + fi + +.PHONY: docker-build-triage-party +docker-build-triage-party: checkout-triage-party + @if [ -z "${TRIAGE_PARTY_VERSION}" ]; then echo "TRIAGE_PARTY_VERSION is not set"; exit 1; fi + cd $(TRIAGE_PARTY_TMP_DIR) && \ + docker buildx build --platform linux/amd64 -t $(TRIAGE_PARTY_CONTROLLER_IMG):$(TRIAGE_PARTY_VERSION) . + +.PHONY: docker-push-triage-party +docker-push-triage-party: + @if [ -z "${TRIAGE_PARTY_VERSION}" ]; then echo "TRIAGE_PARTY_VERSION is not set"; exit 1; fi + docker push $(TRIAGE_PARTY_CONTROLLER_IMG):$(TRIAGE_PARTY_VERSION) + +.PHONY: clean-triage-party +clean-triage-party: + rm -fr "$(TRIAGE_PARTY_TMP_DIR)" + +.PHONY: triage-party +triage-party: ## Start a local instance of triage party + @if [ -z "${GITHUB_TOKEN}" ]; then echo "GITHUB_TOKEN is not set"; exit 1; fi + docker run --platform linux/amd64 --rm \ + -e GITHUB_TOKEN \ + -e "PERSIST_BACKEND=disk" \ + -e "PERSIST_PATH=/app/.cache" \ + -v "$(ROOT_DIR)/$(TRIAGE_PARTY_DIR)/.cache:/app/.cache" \ + -v "$(ROOT_DIR)/$(TRIAGE_PARTY_DIR)/config.yaml:/app/config/config.yaml" \ + -p 8080:8080 \ + $(TRIAGE_PARTY_CONTROLLER_IMG):$(TRIAGE_PARTY_VERSION) + ## -------------------------------------- ## Helpers ## -------------------------------------- diff --git a/hack/tools/triage/.gitignore b/hack/tools/triage/.gitignore new file mode 100644 index 000000000000..7c9e30d2bd98 --- /dev/null +++ b/hack/tools/triage/.gitignore @@ -0,0 +1,2 @@ +triage-party.tmp/ +.cache/ diff --git a/hack/tools/triage/config.yaml b/hack/tools/triage/config.yaml new file mode 100644 index 000000000000..c8a31dcf8e91 --- /dev/null +++ b/hack/tools/triage/config.yaml @@ -0,0 +1,287 @@ +settings: + name: cluster-api + min_similarity: 0.8 + repos: + - https://github.com/kubernetes-sigs/cluster-api + members: + - chrischdi + - enxebre + - fabriziopandini + - killianmuldoon + - sbueringer + - vincepri + +collections: + - id: initial-triage + name: Initial Triage + dedup: false + description: > + Check new issue and apply labels for kind, priority, area (optional). + rules: + - triage-needs-kind + - triage-needs-priority + + - id: triage-refinement + name: Triage Refinement + dedup: false + description: > + Work on issue to get them to an actionable state. + When ready, apply triage/accepted and "help wanted" or "good first issue" label + rules: + - triage-stale-awaiting-more-evidence + - triage-stale-kind-support + - triage-updated + - triage-again + - triage-lifecycle-stale-or-rotten-priority-critical-urgent + - triage-lifecycle-stale-or-rotten-priority-important-soon + - triage-lifecycle-stale-or-rotten-priority-important-longterm + - triage-lifecycle-frozen + - triage-all + + - id: actionable + name: Actionable + description: > + Issues that can be worked on. + dedup: false + rules: + - actionable-not-assigned-without-help-or-good-first-issue + - actionable-updated + - actionable-all + + - id: fix-me + name: Fix-me! + description: > + Ensure we are using the labels in a consistent way. + dedup: false + rules: + - fixme-avoid-triage-needs-information + - fixme-avoid-kind-design + - fixme-kind-proposal-without-kind-feature + - fixme-kind-support-with-other-kinds + - fixme-triage-accepted-with-kind-support-or-priority-awaiting-more-evidence + - fixme-help-or-good-first-issue-before-triage-accepted + - fixme-lifecycle-frozen-with-priority-critical-urgent-priority-important-soon-priority-important-longterm + +rules: + # Phase: Initial triage + + triage-needs-kind: + # WHY?: kinds need to be assigned by maintainers, let's do it! + name: "needs-kind" + resolution: "Add a kind/ label: api-change, bug, cleanup, deprecation, documentation, failing-test, feature, flake, proposal, regression, release-blocking, support" + type: issue + filters: + # TODO: Configure plugins so needs-kind label is applied + use it for this filter + - label: "!kind/.*" + + triage-needs-priority: + # WHY?: kinds need to be assigned by maintainers, let's do it! + name: "needs-priority" + resolution: "Add a priority/ label: priority/critical-urgent > priority/important-soon > priority/important-longterm > priority/backlog > priority/awaiting-more-evidence" + type: issue + filters: + # TODO: Configure plugins so needs-priority label is applied + use it for this filter + - label: "!priority/.*" + + # Phase: Triage finalization + + triage-stale-awaiting-more-evidence: + # WHY?: issue authors are supposed to provide answers timely when an issue is labeled priority/awaiting-more-evidence, otherwise /close + name: "priority/awaiting-more-evidence not commented in the last 15 days" + resolution: "Consider if to close with /close or to remove priority/awaiting-more-evidence label" + type: issue + filters: + - label: "!triage/accepted" + - label: "priority/awaiting-more-evidence" + - commented: +15d + + triage-stale-kind-support: + # WHY?: support request are supposed to be quick back and forth between authors and maintainers, otherwise /close + name: "Support request not commented in the last 15 days" + resolution: "Consider if to close with /close or if to change kind" + type: issue + filters: + - label: "!triage/accepted" + - label: "kind/support" + - commented: +15d + + triage-updated: + # WHY?: maintainers are supposed to keep up with issue discussions and help to move to actionable + name: "Issues with updates in the last month" + resolution: | + Check if the issue is now actionable by applying /triage accepted, help in making progress, consider if to change priority or kind. + Note: maintainers can't make everything actionable (so it is okay if this list does not go to 0). + type: issue + filters: + - label: "!triage/accepted" + - commented: -30d + - tag: "!member-last" + + triage-again: + # WHY?: maintainers are supposed to re-assess triaged issues after some time (frequency depends on priority) + name: "Issues back to triage" + resolution: | + Confirm that this issue is still relevant with /triage accepted or close this issue with /close. + Note: this is a clear sign of project lacking contributors. At some point - might be after two back and forth -, we should give up and close. + Note: it is be okay if this list does not go to 0, maintainers should not be forced to add an answer only for the sake of getting this list empty. + type: issue + filters: + - label: "!triage/accepted" + - un-triaged: +0d + + triage-lifecycle-stale-or-rotten-priority-critical-urgent: + # WHY?: issue with priority/critical-urgent do not get automatically closed, it is up to maintainers to take a look again + name: "Issue with priority/critical-urgent, lifecycle/stale or rotten, and not commented in the last month" + resolution: | + Consider if bring this issue to the attention of the community, consider if to change priority with /priority important-soon, /priority important-longterm or /priority backlog, consider if to close with /close. + Note: this is a clear sign of project lacking contributors. At some point - might be after two back and forth -, we should give up and close. + Note: maintainers can't make everything actionable (so it is okay if this list does not go to 0). + type: issue + filters: + - label: "!triage/accepted" + - label: "priority/critical-urgent" + - label: "lifecycle/(stale|rotten)" + - commented: +30d + + triage-lifecycle-stale-or-rotten-priority-important-soon: + # WHY?: issue with priority/important-soon do not get automatically closed, it is up to maintainers to take a look again + name: "Issue with priority/important-soon, lifecycle/stale or rotten, not commented in the last 2 month" + resolution: | + Consider if to change priority with /priority important-longterm or /priority backlog, consider if to close with /close. + Note: this is a clear sign of project lacking contributors. At some point - might be after two back and forth -, we should give up and close. + Note: maintainers can't make everything actionable (so it is okay if this list does not go to 0). + type: issue + filters: + - label: "!triage/accepted" + - label: "priority/important-soon" + - label: "lifecycle/(stale|rotten)" + - commented: +60d + + triage-lifecycle-stale-or-rotten-priority-important-longterm: + # WHY?: issue with priority/important-longterm do not get automatically closed, it is up to maintainers to take a look again + name: "Issue with priority/important-longterm, lifecycle/stale or rotten, not commented in the last 6 months" + resolution: | + Consider if to change priority with /priority backlog, consider if to close with /close" + Note: this is a clear sign of project lacking contributors. At some point - might be after two back and forth -, we should give up and close. + Note: maintainers can't make everything actionable (so it is okay if this list does not go to 0). + type: issue + filters: + - label: "!triage/accepted" + - label: "priority/important-longterm" + - label: "lifecycle/(stale|rotten)" + - commented: +180d + + triage-lifecycle-frozen: + # WHY?: issue with lifecycle/frozen do not get automatically closed, it is up to maintainers to take a look again + name: "Issue with lifecycle/frozen, not commented in the last 6 months" + resolution: | + Consider if to close with /close. + Note: this is a clear sign of project lacking contributors. At some point - might be after two back and forth -, we should give up and close. + Note: maintainers can't make everything actionable (so it is okay if this list does not go to 0). + type: issue + filters: + - label: "!triage/accepted" + - label: "lifecycle/frozen" + - commented: +180d + + triage-all: + name: "All the issue in triage" + resolution: | + Check if the issue is now actionable by applying /triage accepted, help in making progress, consider if to change priority or kind. + Note: This list is not intended to go to 0 items (it lists all). + type: issue + filters: + - label: "!triage/accepted" + + # Phase: Actionable + + actionable-not-assigned-without-help-or-good-first-issue: + # WHY?: issue actionable must be assigned to someone or seeking for help + name: "Issues not assigned without help or good-first-issue" + resolution: "Apply /help or /good-first-issue" + type: issue + filters: + - label: "triage/accepted" + - label: "!(help wanted|good first issue)" + - tag: "!assigned" + + actionable-updated: + # WHY?: maintainers are supposed to keep up with issue discussions + name: "Issues with updates in the last month" + resolution: | + Check updates + Note: it is be okay if this list does not go to 0, maintainers should not be forced to add an answer only for the sake of getting this list empty. + type: issue + filters: + - label: "triage/accepted" + - commented: -30d + - tag: "!member-last" + + actionable-all: + name: "All the actionable issues" + resolution: | + Note: This list is not intended to go to 0 items (it lists all). + type: issue + filters: + - label: "triage/accepted" + + # Fix-me + # WHY?: Let's try to be diligent in using labels + + fixme-avoid-triage-needs-information: + name: "With triage/needs-information (let's not use this label)" + resolution: "Remove triage/needs-information, use priority/awaiting-more-evidence instead" + type: issue + filters: + - label: "triage/needs-information" + + fixme-avoid-kind-design: + name: "With kind/design (let's not use this label)" + resolution: "remove kind/design, use kind/proposal instead" + type: issue + filters: + - label: "kind/design" + + fixme-kind-proposal-without-kind-feature: + name: "With kind/proposal without kind/feature" + resolution: "Add kind/feature (we usually write proposals for new features, so kind/proposal and kind/feature should go together)" + type: issue + filters: + - label: "kind/proposal" + - label: "!kind/feature" + + fixme-kind-support-with-other-kinds: + name: "With kind/support and also other kinds" + resolution: "Remove incorrect kinds (kind/support should not be combined with other kinds)" + type: issue + filters: + - label: "kind/support" + - label: "kind/(feature|documentation|bug|flake|cleanup|design|proposal|deprecation|regression|api-change|failing-test|release-blocking)" + + fixme-triage-accepted-with-kind-support-or-priority-awaiting-more-evidence: + name: "With triage/accepted and kind/support or priority/awaiting-more-evidence" + resolution: "Remove triage/accepted (triage/accepted should not be applied to issue with kind/support or priority/awaiting-more-evidence)" + type: issue + filters: + - label: "triage/accepted" + - label: "(kind/support|priority/awaiting-more-evidence)" + + fixme-help-or-good-first-issue-before-triage-accepted: + name: "With help or good-first-issue applied before triage/accepted" + resolution: "help or good-first-issue should not be applied before triage/accepted" + type: issue + filters: + - label: "(help wanted|good first issue)" + - label: "!triage/accepted" + - tag: "!untriaged" + + fixme-lifecycle-frozen-with-priority-critical-urgent-priority-important-soon-priority-important-longterm: + name: "With lifecycle/frozen and one of priority/critical-urgent, priority/important-soon or priority/important-longterm" + resolution: "Remove lifecycle/frozen (priority/critical-urgent, priority/important-soon or priority/important-longterm are not subject to lifecycle events)" + type: issue + filters: + - label: "lifecycle/frozen" + - label: "priority/(critical-urgent|important-soon|important-longterm)" + + # TODO: add more checks about invalid combinations of labels + # - ... diff --git a/hack/tools/triage/triage-improvements.patch b/hack/tools/triage/triage-improvements.patch new file mode 100644 index 000000000000..d46b3b863722 --- /dev/null +++ b/hack/tools/triage/triage-improvements.patch @@ -0,0 +1,335 @@ +Subject: [PATCH] triage-improvements +--- +Index: docs/config.md +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/docs/config.md b/docs/config.md +--- a/docs/config.md (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/docs/config.md (date 1713124830293) +@@ -163,12 +163,16 @@ + + # Elapsed time since item was created + - created: [-+]duration # example: +30d +-# Elapsed time since item was updated ++# Elapsed time since item was updated (including events, bots, housekeeping) + - updated: [-+]duration ++# Elapsed time since item was commented (excluding events, bots, housekeeping) ++- commented: [-+]duration + # Elapsed time since item was responded to by a project member + - responded: [-+]duration + # Elapsed time since item was given the current priority + - prioritized: [-+]duration ++# Elapsed time since last time triage/approved was removed from the item ++- un-triaged: [-+]duration + + # Number of reactions this item has received + - reactions: [><=]int # example: +5 +@@ -207,6 +211,7 @@ + * `draft`: PR is a draft PR + * `similar`: the issue or PR appears to be similar to another + * `open-milestone`: the issue or PR appears in an open milestone ++* `untriaged`: triage/accepted label was removed + + To determine review state, we support the following tags: + +Index: pkg/hubbub/match.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/hubbub/match.go b/pkg/hubbub/match.go +--- a/pkg/hubbub/match.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/hubbub/match.go (date 1712951629900) +@@ -23,8 +23,9 @@ + + "github.com/google/triage-party/pkg/provider" + +- "github.com/google/triage-party/pkg/tag" + "k8s.io/klog/v2" ++ ++ "github.com/google/triage-party/pkg/tag" + ) + + // Check if an item matches the filters, pre-comment fetch +@@ -57,12 +58,16 @@ + } + } + +- if f.Responded != "" { +- if ok := matchDuration(i.GetUpdatedAt(), f.Responded); !ok { +- klog.V(2).Infof("#%d update at %s does not meet responded %s", i.GetNumber(), i.GetUpdatedAt(), f.Responded) +- return false +- } +- } ++ // This seems buggy, because it is checking updated (same as above) + there is another check for responded in postFetchMatch which is looking at the corresponding field in the conversation) ++ // Also we want to use responded as latest comment from authors (without housekeeping) ++ /* ++ if f.Responded != "" { ++ if ok := matchDuration(i.GetUpdatedAt(), f.Responded); !ok { ++ klog.V(2).Infof("#%d update at %s does not meet responded %s", i.GetNumber(), i.GetUpdatedAt(), f.Responded) ++ return false ++ } ++ } ++ */ + + if f.Created != "" { + if ok := matchDuration(i.GetCreatedAt(), f.Created); !ok { +@@ -120,6 +125,14 @@ + for _, f := range fs { + klog.V(2).Infof("post-fetch matching item #%d against filter: %+v", co.ID, f) + ++ // We filter Commented by looking at conversations - without bots and housekeeping -. ++ if f.Commented != "" { ++ if ok := matchDuration(co.Commented, f.Commented); !ok { ++ klog.V(2).Infof("#%d commented at %s does not meet %s", co.ID, co.Commented, f.Commented) ++ return false ++ } ++ } ++ + if f.Responded != "" { + if ok := matchDuration(co.LatestMemberResponse, f.Responded); !ok { + klog.V(4).Infof("#%d did not pass matchDuration: %s vs %s", co.ID, co.LatestMemberResponse, f.Responded) +@@ -193,6 +206,13 @@ + return false + } + } ++ ++ if f.UnTriaged != "" { ++ if ok := matchDuration(co.Untriaged, f.UnTriaged); !ok { ++ klog.V(4).Infof("#%d did not pass untriaged duration: %s vs %s", co.ID, co.LatestMemberResponse, f.UnTriaged) ++ return false ++ } ++ } + } + return true + } +Index: pkg/hubbub/conversation.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/hubbub/conversation.go b/pkg/hubbub/conversation.go +--- a/pkg/hubbub/conversation.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/hubbub/conversation.go (date 1712950888889) +@@ -45,6 +45,12 @@ + // Latest comment or event + Updated time.Time `json:"updated"` + ++ // Latest comment (excluding bots and housekeeping) ++ Commented time.Time `json:"responded"` ++ ++ // Latest unlabeled event for triage/accepted label ++ Untriaged time.Time `json:"untriaged"` ++ + // Seen is the age of the data which generated this data + Seen time.Time `json:"seen"` + CommentsSeen int `json:"comments_seen"` +Index: pkg/tag/tag.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/tag/tag.go b/pkg/tag/tag.go +--- a/pkg/tag/tag.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/tag/tag.go (date 1713124794091) +@@ -44,6 +44,7 @@ + AssigneeUpdated = Tag{ID: "assignee-updated", Desc: "Issue has been updated by its assignee", NeedsComments: true} + + // Timeline-based tags ++ UnTriaged = Tag{ID: "untriaged", Desc: "triage/accepted label was removed", NeedsTimeline: true} + XrefApproved = Tag{ID: "pr-approved", Desc: "Last review was an approval", NeedsTimeline: true} + XrefReviewedWithComment = Tag{ID: "pr-reviewed-with-comment", Desc: "Last review was a comment", NeedsTimeline: true} + XrefChangesRequested = Tag{ID: "pr-changes-requested", Desc: "Last review was a request for changes", NeedsTimeline: true} +Index: pkg/provider/filter.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/provider/filter.go b/pkg/provider/filter.go +--- a/pkg/provider/filter.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/provider/filter.go (date 1712951629935) +@@ -45,6 +45,8 @@ + Closed string `yaml:"closed,omitempty"` + Prioritized string `yaml:"prioritized,omitempty"` + Responded string `yaml:"responded,omitempty"` ++ Commented string `yaml:"commented,omitempty"` ++ UnTriaged string `yaml:"un-triaged,omitempty"` + Reactions string `yaml:"reactions,omitempty"` + ReactionsPerMonth string `yaml:"reactions-per-month,omitempty"` + Comments string `yaml:"comments,omitempty"` +Index: pkg/hubbub/issue.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/hubbub/issue.go b/pkg/hubbub/issue.go +--- a/pkg/hubbub/issue.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/hubbub/issue.go (date 1713130113327) +@@ -25,9 +25,10 @@ + "github.com/google/triage-party/pkg/provider" + + "github.com/google/go-github/v33/github" +- "github.com/google/triage-party/pkg/logu" + "gopkg.in/yaml.v2" + "k8s.io/klog/v2" ++ ++ "github.com/google/triage-party/pkg/logu" + ) + + // cachedIssues returns issues, cached if possible +@@ -276,3 +277,23 @@ + + return false + } ++ ++func isHouseKeeping(c *provider.Comment) bool { ++ // skip triage party comments ++ if strings.HasPrefix(strings.TrimSpace(strings.ToLower(c.GetBody())), "triage-party:") { ++ return true ++ } ++ ++ // skip comments with only prow commands (housekeeping) ++ for _, l := range strings.Split(c.GetBody(), "\n") { ++ if strings.TrimSpace(l) == "" { ++ continue ++ } ++ // TODO: consider if we want to make this more string (/kind, /remove kind etc.) ++ if strings.HasPrefix(l, "/") { ++ continue ++ } ++ return false ++ } ++ return true ++} +Index: Dockerfile +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/Dockerfile b/Dockerfile +--- a/Dockerfile (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/Dockerfile (date 1713194203535) +@@ -34,7 +34,7 @@ + COPY pkg ${SRC_DIR}/pkg/ + WORKDIR $SRC_DIR + RUN go mod download +-RUN go build cmd/server/main.go ++RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "${ldflags} -extldflags '-static'" cmd/server/main.go + + # Stage 2: Build the configured application container + FROM gcr.io/distroless/base:latest AS triage-party +Index: pkg/hubbub/timeline.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/hubbub/timeline.go b/pkg/hubbub/timeline.go +--- a/pkg/hubbub/timeline.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/hubbub/timeline.go (date 1713124883324) +@@ -23,8 +23,9 @@ + "github.com/google/triage-party/pkg/persist" + "github.com/google/triage-party/pkg/provider" + +- "github.com/google/triage-party/pkg/tag" + "k8s.io/klog/v2" ++ ++ "github.com/google/triage-party/pkg/tag" + ) + + func (h *Engine) cachedTimeline(ctx context.Context, sp provider.SearchParams) ([]*provider.Timeline, error) { +@@ -102,6 +103,16 @@ + co.Prioritized = t.GetCreatedAt() + } + ++ // We track last time the triage/accepted label was removed ++ if t.GetEvent() == "labeled" && t.GetLabel().GetName() == "triage/accepted" { ++ co.Untriaged = time.Time{} ++ delete(co.Tags, tag.UnTriaged) ++ } ++ if t.GetEvent() == "unlabeled" && t.GetLabel().GetName() == "triage/accepted" { ++ co.Untriaged = t.GetCreatedAt() ++ co.Tags[tag.UnTriaged] = true ++ } ++ + if t.GetEvent() == "cross-referenced" { + if assignedTo[t.GetActor().GetLogin()] { + if t.GetCreatedAt().After(co.LatestAssigneeResponse) { +Index: pkg/hubbub/item.go +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/pkg/hubbub/item.go b/pkg/hubbub/item.go +--- a/pkg/hubbub/item.go (revision 8b4f69ba721123ee3a9289e38ec5ffe084ab66ff) ++++ b/pkg/hubbub/item.go (date 1713127587448) +@@ -21,10 +21,11 @@ + "strings" + "time" + ++ "k8s.io/klog/v2" ++ + "github.com/google/triage-party/pkg/constants" + "github.com/google/triage-party/pkg/provider" + "github.com/google/triage-party/pkg/tag" +- "k8s.io/klog/v2" + ) + + var ( +@@ -102,6 +103,7 @@ + klog.Errorf("debug conversation: %s", formatStruct(co)) + } + ++ var last *provider.Comment + for _, c := range cs { + h.parseRefs(c.Body, co, c.Updated) + if h.debug[co.ID] { +@@ -113,9 +115,21 @@ + continue + } + ++ // We drop housekeeping comments (comments with only prow commands) ++ if isHouseKeeping(c) { ++ continue ++ } ++ ++ last = c ++ + co.LastCommentBody = c.Body + co.LastCommentAuthor = c.User + ++ // We consider commented the time of the last meaningful comment ++ if co.Commented.Before(c.Updated) { ++ co.Commented = c.Updated ++ } ++ + r := c.Reactions + if r.GetTotalCount() > 0 { + co.ReactionsTotal += r.GetTotalCount() +@@ -191,15 +205,17 @@ + } + } + +- if len(cs) > 0 { +- last := cs[len(cs)-1] +- assoc := strings.ToLower(last.AuthorAssoc) +- if assoc == "none" { +- if last.User.GetLogin() == i.GetUser().GetLogin() { +- co.Tags[tag.AuthorLast] = true +- } ++ // We consider last the last meaningful comment (ignoring bots, housekeeping) ++ if last != nil { ++ if last.User.GetLogin() == i.GetUser().GetLogin() { ++ co.Tags[tag.AuthorLast] = true ++ } ++ ++ // We want member last to be consistent with isMember used above ++ if h.isMember(last.User.GetLogin(), last.AuthorAssoc) && !isBot(last.User) { ++ co.Tags[tag.RoleLast("member")] = true + } else { +- co.Tags[tag.RoleLast(assoc)] = true ++ co.Tags[tag.RoleLast("contributor")] = true + } + + if last.Updated.After(co.Updated) {