Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5080a9c
fix(cmt): add /cmt_dispatch and /cleanup_e2e endpoints
May 21, 2026
1dd004a
test(cmt): unit tests for /cmt_dispatch and /cleanup_e2e
May 25, 2026
d0e3d60
fix(e2e): remove dead mw_tracking_key primary cleanup path
May 25, 2026
e0c42f2
fix(e2e): make CMT webhook-driven; stop sending undeclared mw_trackin…
yasserfaraazkhan Jun 1, 2026
83c0fe0
fix(e2e): scope SHA cleanup by flow so concurrent same-SHA runs aren'…
yasserfaraazkhan Jun 1, 2026
a2293fd
chore(cmt): default CMT versions to latest ESR/feature patches (10.11…
yasserfaraazkhan Jun 1, 2026
8af9a48
feat(cmt): raise CMT version cap from 5 to 10
yasserfaraazkhan Jun 1, 2026
48abfce
feat(cmt): auto-derive CMT server versions from Mattermost releases
yasserfaraazkhan Jun 2, 2026
51fb239
Fix non-PR E2E cleanup: key on dispatch-time branch HEAD SHA
yasserfaraazkhan Jun 4, 2026
26da5ac
Auto-expire PR E2E instances and evict their tracking entries
yasserfaraazkhan Jun 4, 2026
5982567
Gate CMT trigger to release branches or manual dispatch
yasserfaraazkhan Jun 5, 2026
6c10c35
CMT: stop sending unused cmt_run_id input on dispatch
yasserfaraazkhan Jun 5, 2026
0db7844
CMT: trigger on RC tag cuts (vX.Y.Z-rc.N) in addition to manual dispatch
yasserfaraazkhan Jun 10, 2026
0ab5fd1
CMT: also trigger on mobile build-release-NNN branches
yasserfaraazkhan Jun 11, 2026
9d50896
push: skip desktop release-branch E2E (replaced by CMT RC-tag trigger)
yasserfaraazkhan Jun 11, 2026
2d3335c
push: remove release-branch E2E trigger (release-X.Y push)
yasserfaraazkhan Jun 11, 2026
afeb5cf
push: skip mobile main-branch E2E (per-PR-merge cost too high)
yasserfaraazkhan Jun 11, 2026
bdb6970
cmt: latest-marker for mobile CMT + remove dead nightly/release-trigg…
yasserfaraazkhan Jun 12, 2026
d65b042
push: drop dead `nightly` param from desktop e2e dispatch
yasserfaraazkhan Jun 12, 2026
4bb788c
Merge origin/master — resolve conflicts
yasserfaraazkhan Jun 26, 2026
8873dff
cmt: race-free dispatch, run-id-keyed cleanup, atomic-write safety
yasserfaraazkhan Jun 29, 2026
70e9c46
cmt: cap matrix at 5 versions; tighten build-release branch gate to 3…
yasserfaraazkhan Jun 29, 2026
91e89a8
e2e: re-enable mobile main-push E2E; remove dead nightly trigger code
yasserfaraazkhan Jun 29, 2026
8f3dbd4
clean up
yasserfaraazkhan Jun 30, 2026
972dbce
cmt: make CMTTestWorkflowName config-driven; drop /cmt_dispatch design
yasserfaraazkhan Jun 30, 2026
d932999
cmt: default CMTTestWorkflowName so cleanup never silently regresses
yasserfaraazkhan Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion config/config-matterwick.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,7 @@
"E2EReleasePatternPrefix": "release-",
"E2ENightlyTriggerWorkflowName": "E2E Nightly Trigger",
"E2ETestWorkflowNames": ["Electron Playwright Tests", "E2E", "Compatibility Matrix Testing"],
"E2EInstanceMaxAge": 6
"E2EInstanceMaxAge": 6,
"CMTTriggerWorkflowName": "CMT Provisioner",
"CMTServerVersions": ["10.11.0", "11.7.0"]
}
26 changes: 26 additions & 0 deletions server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,32 @@ type MatterwickConfig struct {
// Set to the longest expected E2E run duration plus a small buffer.
// Default (0): 3 hours.
E2EInstanceMaxAge int

// CMTTriggerWorkflowName is the workflow name (the "name:" field) of the lightweight,
// scheduled CMT trigger workflow in the desktop/mobile repos. When matterwick receives
// a workflow_run "requested" event for this workflow it provisions one instance per
// version in CMTServerVersions and dispatches compatibility-matrix-testing.yml.
CMTTriggerWorkflowName string

// CMTServerVersions is the hardcoded set of Mattermost server versions CMT runs
// against (e.g. the active ESR plus the current feature release). Values must be valid
// Mattermost image tags (full semver, no "v" prefix, e.g. "10.11.0"). Overridable via
// the gitops config; when empty matterwick falls back to defaultCMTServerVersions.
CMTServerVersions []string
}

// defaultCMTServerVersions is the fallback CMT version set used when Config.CMTServerVersions
// is empty. Mattermost actively supports v11.x feature releases and the v10.11 ESR, so the
// default covers the current ESR line plus a v11 release. Update as ESR/feature lines change.
var defaultCMTServerVersions = []string{"10.11.0", "11.7.0"}

// CMTVersions returns the configured CMT server versions, or the hardcoded default set when
// none are configured.
func (c *MatterwickConfig) CMTVersions() []string {
if len(c.CMTServerVersions) > 0 {
return c.CMTServerVersions
}
return defaultCMTServerVersions
}

func findConfigFile(fileName string) string {
Expand Down
33 changes: 33 additions & 0 deletions server/e2e_dryrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,39 @@ func TestDryRun_InstanceTracking(t *testing.T) {
})
}

// ------------------------------------------------------------
// 9b. SHA-scoped cleanup must not reap a concurrent flow on the same SHA
// ------------------------------------------------------------

func TestInstanceKeyMatchesSHA(t *testing.T) {
repo := "mattermost-mobile"
sha := "deadbeef"
cmtKey := fmt.Sprintf("%s-cmt-100-%s", repo, sha) // CMT flow
nightlyKey := fmt.Sprintf("%s-scheduled-200-%s", repo, sha) // nightly flow, same SHA
pushKey := fmt.Sprintf("%s-push-release-9.0-%s", repo, sha) // push flow, same SHA
prKey := fmt.Sprintf("%s-pr-42", repo) // PR flow, no -sha suffix
otherSHAKey := fmt.Sprintf("%s-cmt-100-%s", repo, "feedface") // CMT, different SHA

t.Run("CMT completion matches only the CMT key", func(t *testing.T) {
assert.True(t, instanceKeyMatchesSHA(cmtKey, repo, sha, true))
assert.False(t, instanceKeyMatchesSHA(nightlyKey, repo, sha, true), "nightly key must survive a CMT completion")
assert.False(t, instanceKeyMatchesSHA(pushKey, repo, sha, true))
assert.False(t, instanceKeyMatchesSHA(prKey, repo, sha, true))
assert.False(t, instanceKeyMatchesSHA(otherSHAKey, repo, sha, true), "different SHA must not match")
})

t.Run("non-CMT completion matches push/scheduled but not CMT", func(t *testing.T) {
assert.False(t, instanceKeyMatchesSHA(cmtKey, repo, sha, false), "CMT key must survive a nightly/push completion")
assert.True(t, instanceKeyMatchesSHA(nightlyKey, repo, sha, false))
assert.True(t, instanceKeyMatchesSHA(pushKey, repo, sha, false))
assert.False(t, instanceKeyMatchesSHA(prKey, repo, sha, false), "PR keys have no -sha suffix")
})

t.Run("other repo is never matched", func(t *testing.T) {
assert.False(t, instanceKeyMatchesSHA("mattermost-desktop-cmt-100-"+sha, repo, sha, true))
})
}

// ------------------------------------------------------------
// 10. Instance name length safety
// ------------------------------------------------------------
Expand Down
26 changes: 10 additions & 16 deletions server/e2e_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -1048,11 +1048,11 @@ func (s *Server) buildInstanceDetailsJSON(instances []*E2EInstance) (string, err
return string(jsonBytes), nil
}

// dispatchDesktopE2EWorkflow triggers the desktop E2E workflow via GitHub Actions API.
// trackingKey is the s.e2eInstances map key for this run; when non-empty it is passed
// as the "mw_tracking_key" workflow input so the workflow_run completed handler can do
// a direct key lookup instead of fragile SHA suffix matching.
func (s *Server) dispatchDesktopE2EWorkflow(repoOwner, repoName, ref, sha, instanceDetailsJSON, runType, trackingKey string, nightly bool) error {
// dispatchDesktopE2EWorkflow triggers the desktop E2E workflow (e2e-functional.yml) via the
// GitHub Actions API. Cleanup is driven by the workflow_run completed event matched on the
// commit SHA, so no tracking key is passed as an input (e2e-functional.yml does not declare
// one, and GitHub rejects a workflow_dispatch carrying an undeclared input with a 422).
func (s *Server) dispatchDesktopE2EWorkflow(repoOwner, repoName, ref, sha, instanceDetailsJSON, runType string, nightly bool) error {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
ctx := context.Background()
client := newGithubClient(s.Config.GithubAccessToken)

Expand Down Expand Up @@ -1085,9 +1085,6 @@ func (s *Server) dispatchDesktopE2EWorkflow(repoOwner, repoName, ref, sha, insta
"run_type": runType,
"nightly": fmt.Sprintf("%t", nightly),
}
if trackingKey != "" {
workflowInputs["mw_tracking_key"] = trackingKey
}

// Use REST API to trigger workflow dispatch (v32 go-github compatibility)
req, err := client.NewRequest("POST",
Expand Down Expand Up @@ -1116,11 +1113,11 @@ func (s *Server) dispatchDesktopE2EWorkflow(repoOwner, repoName, ref, sha, insta
return nil
}

// dispatchMobileE2EWorkflow triggers the mobile E2E workflow via GitHub Actions API.
// trackingKey is the s.e2eInstances map key for this run; when non-empty it is passed
// as the "mw_tracking_key" workflow input so the workflow_run completed handler can do
// a direct key lookup instead of fragile SHA suffix matching.
func (s *Server) dispatchMobileE2EWorkflow(repoOwner, repoName, ref, sha, site1URL, site2URL, site3URL, platform, runType, trackingKey string) error {
// dispatchMobileE2EWorkflow triggers the mobile E2E workflow (e2e-detox-pr.yml) via the
// GitHub Actions API. Cleanup is driven by the workflow_run completed event matched on the
// commit SHA, so no tracking key is passed as an input (e2e-detox-pr.yml does not declare
// one, and GitHub rejects a workflow_dispatch carrying an undeclared input with a 422).
func (s *Server) dispatchMobileE2EWorkflow(repoOwner, repoName, ref, sha, site1URL, site2URL, site3URL, platform, runType string) error {
ctx := context.Background()
client := newGithubClient(s.Config.GithubAccessToken)

Expand All @@ -1138,9 +1135,6 @@ func (s *Server) dispatchMobileE2EWorkflow(repoOwner, repoName, ref, sha, site1U
"PLATFORM": platform,
"run_type": runType,
}
if trackingKey != "" {
workflowInputs["mw_tracking_key"] = trackingKey
}

// Use REST API to trigger workflow dispatch (v32 go-github compatibility)
req, err := client.NewRequest("POST",
Expand Down
18 changes: 9 additions & 9 deletions server/push_events.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (s *Server) handlePushEventE2E(event *github.PushEvent, branch string) {
s.e2eInstances[key] = instances
s.e2eInstancesLock.Unlock()

err = s.triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, sha, key, instances)
err = s.triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, sha, instances)
if err != nil {
logger.WithError(err).Error("Failed to trigger E2E workflow")
s.e2eInstancesLock.Lock()
Expand Down Expand Up @@ -249,8 +249,8 @@ func getRunnerForPlatform(platform string) string {
}

// 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 {
// Cleanup is driven by the workflow_run completed event matched on the commit SHA.
func (s *Server) triggerE2EWorkflowForPushEvent(repoName, instanceType, branch, sha string, instances []*E2EInstance) error {
logger := s.Logger.WithFields(logrus.Fields{
"repo": repoName,
"instanceType": instanceType,
Expand All @@ -265,14 +265,14 @@ func (s *Server) triggerE2EWorkflowForPushEvent(repoName, instanceType, branch,
}

if instanceType == "desktop" {
return s.triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey, instances)
return s.triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, instances)
}

return s.triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey, instances)
return s.triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, instances)
}

// triggerDesktopE2EWorkflowForPushEvent dispatches the desktop E2E workflow.
func (s *Server) triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey string, instances []*E2EInstance) error {
func (s *Server) triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha string, instances []*E2EInstance) error {
logger := s.Logger.WithFields(logrus.Fields{
"repo": repoName,
"branch": branch,
Expand All @@ -291,11 +291,11 @@ func (s *Server) triggerDesktopE2EWorkflowForPushEvent(repoOwner, repoName, bran
runType = "RELEASE"
}

return s.dispatchDesktopE2EWorkflow(repoOwner, repoName, branch, sha, instanceDetailsJSON, runType, trackingKey, false)
return s.dispatchDesktopE2EWorkflow(repoOwner, repoName, branch, sha, instanceDetailsJSON, runType, false)
}

// triggerMobileE2EWorkflowForPushEvent dispatches the mobile E2E workflow (e2e-detox-pr.yml).
func (s *Server) triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha, trackingKey string, instances []*E2EInstance) error {
func (s *Server) triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branch, sha string, instances []*E2EInstance) error {
logger := s.Logger.WithFields(logrus.Fields{
"repo": repoName,
"branch": branch,
Expand All @@ -321,6 +321,6 @@ func (s *Server) triggerMobileE2EWorkflowForPushEvent(repoOwner, repoName, branc
repoOwner, repoName, branch, sha,
instances[0].URL, instances[1].URL, instances[2].URL,
"both", // push events always test both iOS and Android
runType, trackingKey,
runType,
)
}
Loading
Loading