diff --git a/modules/gitea/cmd/agent-teams/retire.go b/modules/gitea/cmd/agent-teams/retire.go index 8da05e440..0ed7b2c8c 100644 --- a/modules/gitea/cmd/agent-teams/retire.go +++ b/modules/gitea/cmd/agent-teams/retire.go @@ -19,10 +19,17 @@ import ( // seats came, made each consumer of npm-package-registry an account under the mesh's derived login // (mesh__) and put it in the organisation's team packages (read and write) or readers (read only). // Reads of the packages are anonymous and every write goes through the seats, so both teams go, and each account of -// that form in no team but them goes with them. Nothing else is touched: a team of another name, an account of -// another form or one still in a team the module does not retire stays as it is. +// that form in no team but them goes with them. Only what the provisioner made goes: a team of those names in its +// organisation (RetiredOrg) is the provisioner's when every member of it is an account of the provisioner's form and +// it has one at least, since the provisioner put its own accounts in them alone and nobody else ever made one. A team +// a person makes later under such a name holds no such account (they are gone with the first run), so it stays, and +// so does a team of those names with a person in it or with no member. Nothing else is touched: a team of another +// name or organisation, an account of another form or one still in a team the module does not retire stays as it is. var RetiredTeams = []string{"packages", "readers"} +// RetiredOrg is the organisation the npm pair provisioner made its teams in (its ORG, the npm registry's owner). +const RetiredOrg = "novox" + // retiredAccount is the login form the npm pair provisioner made its accounts under. var retiredAccount = regexp.MustCompile(`^mesh_[a-z0-9-]+_[a-z0-9_]+$`) @@ -37,6 +44,7 @@ func (s *Syncer) Retire(ctx context.Context, orgs []string) error { team forgeTeam } var gone []retired + retiring := map[string]bool{} // org/team for _, org := range orgs { teams, err := s.teams(ctx, org) if err != nil { @@ -50,15 +58,14 @@ func (s *Syncer) Retire(ctx context.Context, orgs []string) error { for _, m := range members { teamsOf[m.Login] = append(teamsOf[m.Login], org+"/"+t.Name) } - if slices.Contains(RetiredTeams, t.Name) { + made := len(members) > 0 && !slices.ContainsFunc(members, func(m forgeMember) bool { return !retiredAccount.MatchString(m.Login) }) + if org == RetiredOrg && slices.Contains(RetiredTeams, t.Name) && made { gone = append(gone, retired{org, t}) + retiring[org+"/"+t.Name] = true } } } - isRetired := func(orgTeam string) bool { - _, team, _ := strings.Cut(orgTeam, "/") - return slices.Contains(RetiredTeams, team) - } + isRetired := func(orgTeam string) bool { return retiring[orgTeam] } var accounts []string for login, in := range teamsOf { if retiredAccount.MatchString(login) && slices.ContainsFunc(in, isRetired) && !slices.ContainsFunc(in, func(t string) bool { return !isRetired(t) }) { diff --git a/modules/gitea/cmd/agent-teams/teams_test.go b/modules/gitea/cmd/agent-teams/teams_test.go index a68007579..c5e746a19 100644 --- a/modules/gitea/cmd/agent-teams/teams_test.go +++ b/modules/gitea/cmd/agent-teams/teams_test.go @@ -365,11 +365,15 @@ func TestReadValue(t *testing.T) { // form still in another team, an account of another form and every other team stay; a second run changes nothing. func TestTheNpmPairsTeamsAndAccountsAreRetired(t *testing.T) { f := newFake([]string{"novox"}, "mesh_ace_mesh_build_node", "mesh_shanks_lab", "mesh_g14_agent", "mesh_novox_owner", - "mesh-packages", "agent-ace", "jschoubben") + "mesh-packages", "agent-ace", "jschoubben", "mesh_ace_agent") f.owners = map[string]bool{"mesh_novox_owner": true} - f.add("novox", "packages", "none", nil, []string{"mesh_ace_mesh_build_node", "mesh_g14_agent", "mesh_novox_owner", "jschoubben"}) + f.add("novox", "packages", "none", nil, []string{"mesh_ace_mesh_build_node", "mesh_g14_agent", "mesh_novox_owner"}) f.add("novox", "readers", "none", nil, []string{"mesh_ace_mesh_build_node", "mesh_shanks_lab"}) f.add("novox", "agents", "none", nil, []string{"mesh_g14_agent", "agent-ace"}) + // Catalog note: a team of a retired name the provisioner did not make (a person in it, or in another + // organisation) stays. + f.orgs = append(f.orgs, "other") + f.add("other", "readers", "none", nil, []string{"mesh_ace_agent"}) f.add("novox", "mesh-packages", "none", nil, []string{"mesh-packages"}) s, said := syncer(t, f) if err := s.Sync(context.Background(), Value{}); err != nil { @@ -382,7 +386,7 @@ func TestTheNpmPairsTeamsAndAccountsAreRetired(t *testing.T) { t.Errorf("a team the module did not retire was removed: %+v", f.teams) } for u, want := range map[string]bool{"mesh_ace_mesh_build_node": false, "mesh_shanks_lab": false, "mesh_g14_agent": true, - "mesh_novox_owner": true, "mesh-packages": true, "agent-ace": true, "jschoubben": true} { + "mesh_novox_owner": true, "mesh-packages": true, "agent-ace": true, "jschoubben": true, "mesh_ace_agent": true} { if f.users[u] != want { t.Errorf("account %s kept %v, want %v", u, f.users[u], want) } @@ -390,8 +394,17 @@ func TestTheNpmPairsTeamsAndAccountsAreRetired(t *testing.T) { if !f.prohibited["mesh_novox_owner"] || len(f.prohibited) != 1 { t.Errorf("prohibited %v, want mesh_novox_owner alone", f.prohibited) } + if f.byName("other", "readers") == nil { + t.Errorf("a readers team in another organisation was removed") + } + // A team a person makes after the first run under a retired name, with persons in it or none, stays. + f.add("novox", "packages", "none", nil, []string{"jschoubben"}) + f.add("novox", "readers", "none", nil, nil) before := len(*said) if err := s.Sync(context.Background(), Value{}); err != nil || len(*said) != before { t.Errorf("a second run: %v, said %v; want nothing", err, (*said)[before:]) } + if f.byName("novox", "packages") == nil || f.byName("novox", "readers") == nil || !f.users["jschoubben"] { + t.Errorf("a team a person made later under a retired name was removed: %+v", f.teams) + } }