diff --git a/config/config-matterwick.default.json b/config/config-matterwick.default.json index 1b7ed1a..08816fc 100644 --- a/config/config-matterwick.default.json +++ b/config/config-matterwick.default.json @@ -74,11 +74,14 @@ "E2ELabel": "E2E/Run", "E2EMobileIOSLabel": "E2E/Run-iOS", "E2EMobileAndroidLabel": "E2E/Run-Android", + "E2EResetServersLabel": "E2E/Reset-Servers", "E2EUsername": "admin", + "E2EPassword": "", "E2EServerVersion": "latest", "E2EAutoTriggerOnRelease": true, "E2EAutoTriggerOnMaster": true, "E2EReleasePatternPrefix": "release-", "E2ENightlyTriggerWorkflowName": "E2E Nightly Trigger", - "E2ETestWorkflowNames": ["Electron Playwright Tests", "E2E", "Compatibility Matrix Testing"] + "E2ETestWorkflowNames": ["Electron Playwright Tests", "E2E", "Compatibility Matrix Testing"], + "E2EInstanceMaxAge": 6 } diff --git a/server/e2e_dryrun_test.go b/server/e2e_dryrun_test.go index 6b5f488..d778257 100644 --- a/server/e2e_dryrun_test.go +++ b/server/e2e_dryrun_test.go @@ -421,10 +421,22 @@ func TestDryRun_DesktopPushEvent(t *testing.T) { assert.False(t, s.isReleaseBranch("feature-branch")) }) - t.Run("version extracted from release branch", func(t *testing.T) { - assert.Equal(t, "8.0", extractVersionFromReleaseBranch("release-8.0", "release-")) - assert.Equal(t, "10.5", extractVersionFromReleaseBranch("release-10.5", "release-")) - assert.Equal(t, "", extractVersionFromReleaseBranch("master", "release-")) + t.Run("empty release pattern prefix never matches any branch", func(t *testing.T) { + // strings.HasPrefix(any, "") is always true. Without this guard, a missing + // or empty E2EReleasePatternPrefix in the deployed config would classify + // every push (master, feature branches, anything) as a release branch and + // trigger spurious E2E provisioning on every push. + empty := newDryRunServer(t, "", "mattermost") + empty.Config.E2EReleasePatternPrefix = "" + + assert.False(t, empty.isReleaseBranch("master"), + "empty prefix must not match master") + assert.False(t, empty.isReleaseBranch("release-8.0"), + "empty prefix must not match release-8.0") + assert.False(t, empty.isReleaseBranch("anything"), + "empty prefix must not match arbitrary branches") + assert.False(t, empty.isReleaseBranch(""), + "empty prefix must not match empty branch") }) t.Run("branch name extracted from git ref", func(t *testing.T) { @@ -433,6 +445,18 @@ func TestDryRun_DesktopPushEvent(t *testing.T) { assert.Equal(t, "feature/my-branch", extractBranchName("refs/heads/feature/my-branch")) }) + t.Run("tag refs are not treated as branch refs", func(t *testing.T) { + // extractBranchName("refs/tags/release-9.0") returns "release-9.0", + // which would match isReleaseBranch and trigger unintended E2E provisioning. + // handlePushEvent must guard against non-refs/heads/ refs before calling extractBranchName. + tagRef := "refs/tags/release-9.0" + assert.False(t, strings.HasPrefix(tagRef, "refs/heads/"), + "tag ref must be filtered before branch-trigger evaluation") + // The extracted value looks like a release branch — the guard is what prevents it. + assert.Equal(t, "release-9.0", extractBranchName(tagRef), + "extractBranchName is unaware of ref type; caller must pre-filter") + }) + t.Run("desktop push always creates linux/macos/windows instances", func(t *testing.T) { // createMultipleE2EInstancesForPushEvent uses desktop platforms for push events expectedPlatforms := []string{"linux", "macos", "windows"} @@ -909,6 +933,33 @@ func TestDryRun_ResolveE2EServerVersion(t *testing.T) { assert.False(t, called, "GitHub API must not be called when E2EServerVersion is not 'latest'") }) + t.Run("empty config falls back to latest resolution, not empty version", func(t *testing.T) { + // A missing E2EServerVersion field in the deployed config decodes to "". + // Before the fix, "" was returned as-is and flowed to CreateInstallation, + // which silently failed to provision any server. Empty must be treated + // as "latest" so the GitHub-releases lookup runs. + body := `[{"tag_name":"v12.0.0","draft":false,"prerelease":false}]` + srv := mockReleasesServer(t, body, http.StatusOK) + s := newDryRunServer(t, "", "mattermost") + s.Config.E2EServerVersion = "" + s.githubAPIBase = srv.URL + "/" + + assert.Equal(t, "12.0.0", s.resolveE2EServerVersion(), + "empty E2EServerVersion must fall back to latest resolution, not return empty") + }) + + t.Run("whitespace-only config falls back to latest resolution", func(t *testing.T) { + // Defensive: treat whitespace as empty for the same reason — a config-edit + // typo with stray whitespace should not silently break provisioning. + body := `[{"tag_name":"v12.0.0","draft":false,"prerelease":false}]` + srv := mockReleasesServer(t, body, http.StatusOK) + s := newDryRunServer(t, "", "mattermost") + s.Config.E2EServerVersion = " " + s.githubAPIBase = srv.URL + "/" + + assert.Equal(t, "12.0.0", s.resolveE2EServerVersion()) + }) + t.Run("RC tags skipped, first stable tag returned with v stripped", func(t *testing.T) { body := `[ {"tag_name":"v11.7.0-rc2","draft":false}, @@ -1087,6 +1138,37 @@ func TestDryRun_ResolveE2EServerVersion(t *testing.T) { }) } +// ------------------------------------------------------------ +// 12b. Push-event server version selection — always latest, ignore branch suffix +// ------------------------------------------------------------ + +func TestDryRun_PushEventServerVersion(t *testing.T) { + // Push events (release branch, master, main) must always provision the latest + // stable Mattermost release. Deriving a version from a release branch name + // (e.g. "release-9.0" → "9.0") would attempt to pull a Docker tag that + // typically doesn't exist (Docker Hub publishes full SemVer like "9.0.0"), + // causing silent installation failures and no E2E workflow dispatch. + t.Run("release branch push uses latest version, ignoring branch-derived version", func(t *testing.T) { + body := `[{"tag_name":"v12.0.0","draft":false,"prerelease":false}]` + srv := mockReleasesServer(t, body, http.StatusOK) + s := newDryRunServerLatest(t, srv) + + assert.Equal(t, "12.0.0", s.serverVersionForPushEvent("release-9.0"), + "release-9.0 push must provision latest server (12.0.0), not branch-derived 9.0") + assert.Equal(t, "12.0.0", s.serverVersionForPushEvent("release-10.5"), + "release-10.5 push must provision latest server (12.0.0), not branch-derived 10.5") + }) + + t.Run("master push uses latest version", func(t *testing.T) { + body := `[{"tag_name":"v12.0.0","draft":false,"prerelease":false}]` + srv := mockReleasesServer(t, body, http.StatusOK) + s := newDryRunServerLatest(t, srv) + + assert.Equal(t, "12.0.0", s.serverVersionForPushEvent("master")) + assert.Equal(t, "12.0.0", s.serverVersionForPushEvent("main")) + }) +} + // ------------------------------------------------------------ // 13. MM_SERVER_VERSION sourced from instance, not config // ------------------------------------------------------------ diff --git a/server/e2e_tests.go b/server/e2e_tests.go index 8f3601c..f0efae3 100644 --- a/server/e2e_tests.go +++ b/server/e2e_tests.go @@ -769,18 +769,18 @@ func (s *Server) cleanupStaleNonPRE2EInstances() { logger.Info("Non-PR E2E instance cleanup scan complete") } -// resolveE2EServerVersion returns the Mattermost server version to use for E2E instances. -// If E2EServerVersion is "latest", it fetches the mattermost/mattermost GitHub releases, -// skips drafts, prerelease-flagged releases, and RC/beta/alpha tag-name patterns, then -// returns the first (newest) fully stable tag stripped of its "v" prefix -// (e.g. "v11.6.0" → "11.6.0") to match the Docker Hub tag format. -// -// The resolved version is cached in memory for 1 hour so that back-to-back provisioning -// calls (e.g. three parallel platform instances) share a single GitHub API round-trip. -// Falls back to "master" on any API error or when no stable release is found. +// resolveE2EServerVersion returns the server version for E2E provisioning. +// "latest" (or empty) fetches the newest stable mattermost/mattermost release — skips +// drafts, prereleases, and RC/beta/alpha tags — strips the "v" prefix, and caches the +// result for 1 hour. Falls back to "master" on API error or if no stable release is found. func (s *Server) resolveE2EServerVersion() string { - if s.Config.E2EServerVersion != "latest" { - return s.Config.E2EServerVersion + cfg := strings.TrimSpace(s.Config.E2EServerVersion) + if cfg == "" { + s.Logger.Warn("[resolveE2EServerVersion] E2EServerVersion is empty in config; defaulting to 'latest'") + cfg = "latest" + } + if cfg != "latest" { + return cfg } const cacheTTL = 1 * time.Hour @@ -795,14 +795,12 @@ func (s *Server) resolveE2EServerVersion() string { } s.e2eVersionCacheLock.Unlock() - // 10-second timeout prevents blocking instance-creation goroutines indefinitely - // if the GitHub API is slow or unreachable. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() client := newGithubClient(s.Config.GithubAccessToken) - // Redirect to a mock server when running tests (githubAPIBase is empty in production). + // githubAPIBase is only set in tests to redirect to a mock server. if s.githubAPIBase != "" { if baseURL, parseErr := url.Parse(s.githubAPIBase); parseErr == nil { client.BaseURL = baseURL @@ -826,18 +824,15 @@ func (s *Server) resolveE2EServerVersion() string { } for _, r := range releases { - // Skip drafts and GitHub's explicit prerelease flag first. if r.Draft || r.Prerelease { continue } - // Also skip by tag-name pattern as a secondary guard for releases whose - // prerelease flag may not be set correctly (e.g. some RC tags). + // Secondary guard: some RC tags don't have the prerelease flag set correctly. lower := strings.ToLower(r.TagName) if strings.Contains(lower, "-rc") || strings.Contains(lower, "-beta") || strings.Contains(lower, "-alpha") { continue } - // Strip "v" prefix to match Docker Hub tag format (e.g. "v11.6.0" → "11.6.0"). - version := strings.TrimPrefix(r.TagName, "v") + version := strings.TrimPrefix(r.TagName, "v") // Docker Hub uses "11.6.0" not "v11.6.0" s.Logger.WithField("version", version).Info("[resolveE2EServerVersion] Resolved latest Mattermost server version") s.e2eVersionCacheLock.Lock() diff --git a/server/push_events.go b/server/push_events.go index af55513..685a269 100644 --- a/server/push_events.go +++ b/server/push_events.go @@ -13,10 +13,23 @@ import ( "github.com/sirupsen/logrus" ) -// handlePushEvent processes push events to trigger E2E tests on release branches or master/main +// handlePushEvent triggers E2E tests on release branches or master/main pushes. func (s *Server) handlePushEvent(event *github.PushEvent) { repoName := event.GetRepo().GetName() - branchRef := event.GetRef() // Format: "refs/heads/branch-name" + branchRef := event.GetRef() + + // Ignore tag pushes (refs/tags/*) — only branch pushes should trigger E2E. + // Without this guard, a tag like "refs/tags/release-9.0" would pass through + // extractBranchName as "release-9.0" and accidentally match isReleaseBranch. + if !strings.HasPrefix(branchRef, "refs/heads/") { + s.Logger.WithFields(logrus.Fields{ + "repo": repoName, + "ref": branchRef, + "action": "push", + }).Info("Push ref is not a branch, skipping E2E trigger") + return + } + branch := extractBranchName(branchRef) logger := s.Logger.WithFields(logrus.Fields{ @@ -26,31 +39,43 @@ func (s *Server) handlePushEvent(event *github.PushEvent) { }) logger.Info("Push event received") - // Detect if this is a release branch push if s.Config.E2EAutoTriggerOnRelease && s.isReleaseBranch(branch) { logger.WithField("type", "release_branch").Info("Release branch detected, triggering E2E tests") - version := extractVersionFromReleaseBranch(branch, s.Config.E2EReleasePatternPrefix) - go s.handlePushEventE2E(event, branch, version) + go s.handlePushEventE2E(event, branch) return } - // Detect if this is a master/main branch push if s.Config.E2EAutoTriggerOnMaster && (branch == "master" || branch == "main") { logger.WithField("type", "master_main").Info("Master/main branch detected, triggering E2E tests") - go s.handlePushEventE2E(event, branch, "") + go s.handlePushEventE2E(event, branch) return } - logger.Debug("Push event does not match E2E trigger conditions") + logger.WithFields(logrus.Fields{ + "auto_release": s.Config.E2EAutoTriggerOnRelease, + "auto_master": s.Config.E2EAutoTriggerOnMaster, + "release_pattern_prefix": s.Config.E2EReleasePatternPrefix, + "is_release_branch": s.isReleaseBranch(branch), + }).Info("Push event does not match E2E trigger conditions") } -// isReleaseBranch checks if a branch name is a release branch +// isReleaseBranch returns true if branch matches E2EReleasePatternPrefix. +// Rejects empty prefix — strings.HasPrefix(x, "") is always true. func (s *Server) isReleaseBranch(branch string) bool { + if s.Config.E2EReleasePatternPrefix == "" { + return false + } return strings.HasPrefix(branch, s.Config.E2EReleasePatternPrefix) } -// extractBranchName extracts the branch name from the ref -// GitHub sends refs in the format "refs/heads/branch-name" +// serverVersionForPushEvent resolves the server version via resolveE2EServerVersion. +// Branch name is ignored — derived names like "9.0" don't exist as Docker Hub tags ("9.0.0"). +func (s *Server) serverVersionForPushEvent(branch string) string { + _ = branch + return s.resolveE2EServerVersion() +} + +// extractBranchName extracts the branch name from "refs/heads/branch-name". func extractBranchName(ref string) string { parts := strings.Split(ref, "/") if len(parts) < 3 { @@ -59,17 +84,9 @@ func extractBranchName(ref string) string { return strings.Join(parts[2:], "/") } -// extractVersionFromReleaseBranch extracts version from release branch name -// Example: "release-8.0" -> "8.0" -func extractVersionFromReleaseBranch(branch string, prefix string) string { - if !strings.HasPrefix(branch, prefix) { - return "" - } - return strings.TrimPrefix(branch, prefix) -} - -// handlePushEventE2E orchestrates E2E testing for push events (release or master/main) -func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string, version string) { +// handlePushEventE2E provisions E2E servers and dispatches the test workflow +// for a push to a release branch or master/main. Only acts on desktop/mobile repos. +func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string) { repoName := event.GetRepo().GetName() commit := event.GetHeadCommit() sha := "" @@ -78,13 +95,11 @@ func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string, vers } logger := s.Logger.WithFields(logrus.Fields{ - "repo": repoName, - "branch": branch, - "version": version, - "sha": sha, + "repo": repoName, + "branch": branch, + "sha": sha, }) - // Determine if this is a desktop or mobile repository isDesktop := strings.Contains(repoName, "desktop") isMobile := strings.Contains(repoName, "mobile") @@ -93,14 +108,11 @@ func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string, vers return } - // Create E2E instances for testing instanceType := "desktop" if isMobile { instanceType = "mobile" } - // Validate SHA before provisioning — an empty SHA would produce a malformed - // tracking key and send an empty MOBILE_VERSION to the test workflow. if sha == "" { logger.Error("Push event has no commit SHA, skipping E2E dispatch") return @@ -108,8 +120,7 @@ func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string, vers logger.WithField("instanceType", instanceType).Info("Creating E2E instances for push event") - // Create instances based on repo type - instances, err := s.createMultipleE2EInstancesForPushEvent(repoName, instanceType, branch, version, sha) + instances, err := s.createMultipleE2EInstancesForPushEvent(repoName, instanceType, branch) if err != nil { logger.WithError(err).Error("Failed to create E2E instances") return @@ -122,19 +133,16 @@ func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string, vers logger.WithField("instanceCount", len(instances)).Info("E2E instances created successfully") - // Track instances BEFORE dispatching so that a fast-completing workflow_run - // completed event cannot race ahead of us and find nothing to clean up. + // Store instances before dispatching so a fast-completing workflow_run event + // doesn't race ahead and find nothing to clean up. key := fmt.Sprintf("%s-push-%s-%s", repoName, branch, sha) s.e2eInstancesLock.Lock() s.e2eInstances[key] = instances s.e2eInstancesLock.Unlock() - // Trigger the appropriate E2E workflow, passing the tracking key so it reaches - // the workflow inputs as mw_tracking_key for reliable cleanup on completion. err = s.triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, sha, key, instances) if err != nil { logger.WithError(err).Error("Failed to trigger E2E workflow") - // Remove from tracking and destroy instances on dispatch failure s.e2eInstancesLock.Lock() delete(s.e2eInstances, key) s.e2eInstancesLock.Unlock() @@ -147,7 +155,7 @@ func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string, vers // createMultipleE2EInstancesForPushEvent creates all platform instances in parallel. // Results are returned in platforms[] order so index-based assignment is stable. -func (s *Server) createMultipleE2EInstancesForPushEvent(repoName, instanceType, branch, version, _ string) ([]*E2EInstance, error) { +func (s *Server) createMultipleE2EInstancesForPushEvent(repoName, instanceType, branch string) ([]*E2EInstance, error) { var platforms []string if instanceType == "desktop" { platforms = []string{"linux", "macos", "windows"} @@ -161,11 +169,7 @@ func (s *Server) createMultipleE2EInstancesForPushEvent(repoName, instanceType, "platformCount": len(platforms), }) - // Name format: {type}-{version}-{platform}-{hex6} - serverVersion := s.resolveE2EServerVersion() - if version != "" { - serverVersion = version - } + serverVersion := s.serverVersionForPushEvent(branch) sanitizedVersion := sanitizeForDNS(serverVersion) uid := e2eUniqueSuffix() @@ -227,7 +231,10 @@ func (s *Server) createMultipleE2EInstancesForPushEvent(repoName, instanceType, return instances, nil } -// getRunnerForPlatform returns the GitHub Actions runner for a given platform +// getRunnerForPlatform returns the GitHub Actions runner label for E2E functional +// workflows (PR label + push events). CMT workflows use a separate hardcoded matrix +// in buildDesktopCMTMatrixJSON (macos-13) because compatibility testing pins a +// specific OS version; functional tests track latest. func getRunnerForPlatform(platform string) string { switch strings.ToLower(platform) { case "linux": @@ -241,9 +248,8 @@ func getRunnerForPlatform(platform string) string { } } -// triggerE2EWorkflowForPushEvent triggers the E2E workflow for a push event. -// trackingKey is threaded through to the dispatch call so it appears as mw_tracking_key -// in the workflow inputs, enabling direct key-based cleanup on completion. +// triggerE2EWorkflowForPushEvent routes to the desktop or mobile dispatch function. +// trackingKey is embedded in workflow inputs as mw_tracking_key for cleanup on completion. func (s *Server) triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, sha, trackingKey string, instances []*E2EInstance) error { logger := s.Logger.WithFields(logrus.Fields{ "repo": repoName, @@ -252,7 +258,6 @@ func (s *Server) triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, "sha": sha, }) - // Determine repo owner repoOwner := s.Config.Org if repoOwner == "" { logger.Error("Organization not configured") @@ -266,14 +271,13 @@ func (s *Server) triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, return s.triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey, instances) } -// triggerDesktopE2EWorkflowForPushEvent triggers the desktop E2E workflow +// triggerDesktopE2EWorkflowForPushEvent dispatches the desktop E2E workflow. func (s *Server) triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey string, instances []*E2EInstance) error { logger := s.Logger.WithFields(logrus.Fields{ "repo": repoName, "branch": branch, }) - // Build instance details JSON for desktop workflow instanceDetailsJSON, err := s.buildInstanceDetailsJSON(instances) if err != nil { logger.WithError(err).Error("Failed to build instance details JSON") @@ -290,7 +294,7 @@ func (s *Server) triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, bran return s.dispatchDesktopE2EWorkflow(repoOwner, repoName, branch, sha, instanceDetailsJSON, runType, trackingKey, false) } -// triggerMobileE2EWorkflowForPushEvent triggers the mobile E2E workflow (e2e-detox-pr.yml) +// triggerMobileE2EWorkflowForPushEvent dispatches the mobile E2E workflow (e2e-detox-pr.yml). func (s *Server) triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey string, instances []*E2EInstance) error { logger := s.Logger.WithFields(logrus.Fields{ "repo": repoName, @@ -306,7 +310,7 @@ func (s *Server) triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branc "site_1_url": instances[0].URL, "site_2_url": instances[1].URL, "site_3_url": instances[2].URL, - }).Debug("Triggering mobile E2E workflow (e2e-detox-pr.yml) for push event") + }).Debug("Triggering mobile E2E workflow for push event") runType := "MASTER" if s.isReleaseBranch(branch) { @@ -316,8 +320,7 @@ func (s *Server) triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branc return s.dispatchMobileE2EWorkflow( repoOwner, repoName, branch, sha, instances[0].URL, instances[1].URL, instances[2].URL, - "both", // Push events (release/master) test both platforms + "both", // push events always test both iOS and Android runType, trackingKey, ) } - diff --git a/server/workflow_run.go b/server/workflow_run.go index fc35b4a..eba97da 100644 --- a/server/workflow_run.go +++ b/server/workflow_run.go @@ -43,8 +43,7 @@ func ParseWorkflowRunEventWithInputs(data io.Reader) (*WorkflowRunWebhookPayload return &payload, nil } -// handleWorkflowRunEventWithInputs handles GitHub workflow_run events. -// Routes events to CMT, nightly trigger, or test-workflow completion handlers. +// handleWorkflowRunEventWithInputs routes workflow_run events to CMT, nightly, or cleanup handlers. func (s *Server) handleWorkflowRunEventWithInputs(payload *WorkflowRunWebhookPayload) { // Extract repository info repoData := payload.Repository @@ -79,10 +78,7 @@ func (s *Server) handleWorkflowRunEventWithInputs(payload *WorkflowRunWebhookPay "head_sha": headSHA, }) - // --- CMT trigger workflows --- - // "CMT Provisioner" (name contains "CMT") is dispatched by users with server_versions. - // "Compatibility Matrix Testing" (the actual test workflow) is dispatched by Matterwick - // with CMT_MATRIX; its completion triggers sha-based cleanup via isE2ETestWorkflow. + // CMT: "CMT Provisioner" (user-dispatched) provisions servers; "Compatibility Matrix Testing" runs tests. if strings.Contains(workflowName, "cmt") || strings.Contains(workflowName, "CMT") { if payload.Action == "completed" { logger.Debug("CMT trigger workflow completed; sha-based cleanup is primary") @@ -118,9 +114,7 @@ func (s *Server) handleWorkflowRunEventWithInputs(payload *WorkflowRunWebhookPay return } - // --- Nightly trigger workflow --- - // When the lightweight nightly trigger workflow is requested, provision instances and - // dispatch the actual test workflow. The nightly trigger workflow does no testing itself. + // Nightly: lightweight trigger workflow fires first; matterwick provisions instances and dispatches the real test workflow. if s.Config.E2ENightlyTriggerWorkflowName != "" && workflowName == s.Config.E2ENightlyTriggerWorkflowName { if payload.Action == "requested" { logger.Info("Nightly trigger workflow started, provisioning E2E servers") @@ -133,10 +127,7 @@ func (s *Server) handleWorkflowRunEventWithInputs(payload *WorkflowRunWebhookPay if payload.Action == "completed" && s.isE2ETestWorkflow(workflowName) { logger.Info("Test workflow completed, checking for instance cleanup") - // Primary path: direct tracking-key lookup using the key embedded in dispatch - // inputs. Immune to SHA mismatch (new commits during ~30 min provisioning window) - // and to runID collisions (two runs sharing the same branch HEAD sha). - // Reading a nil Inputs map in Go returns "" safely — no nil check required. + // Primary: look up by mw_tracking_key embedded at dispatch time (immune to SHA races). if trackingKey := payload.WorkflowRun.Inputs["mw_tracking_key"]; trackingKey != "" { s.e2eInstancesLock.Lock() instances := s.e2eInstances[trackingKey] @@ -151,13 +142,16 @@ func (s *Server) handleWorkflowRunEventWithInputs(payload *WorkflowRunWebhookPay return } - // Fallback: SHA-based scan for runs dispatched before mw_tracking_key was added. + // Fallback: SHA-based scan (runs dispatched before mw_tracking_key was introduced). logger.Debug("No mw_tracking_key in workflow inputs, falling back to SHA-based instance cleanup") s.findAndDestroyInstancesBySHA(repoName, headSHA, logger) return } - logger.Debug("Ignoring workflow_run event (not relevant to E2E lifecycle)") + logger.WithFields(logrus.Fields{ + "configured_nightly_name": s.Config.E2ENightlyTriggerWorkflowName, + "configured_test_workflows": s.Config.E2ETestWorkflowNames, + }).Info("Ignoring workflow_run event (not relevant to E2E lifecycle)") } // handleNightlyE2ETrigger provisions instances and dispatches the test workflow.