Merge pull request 'The forge module changes no repository's branch protection unless keep-main-rules is true (issue 1903)' (#463) from fix/gitea-keep-main-rules into main
Reviewed-on: #463
This commit was merged in pull request #463.
This commit is contained in:
@@ -320,7 +320,7 @@ func syncer(t *testing.T, f *fakeForge) (*Syncer, *[]string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var said []string
|
||||
return &Syncer{Forge: forgehttp.Client{URL: srv.URL, User: admin, PasswordFile: pw}, Org: org, Admin: admin, Tools: tools,
|
||||
return &Syncer{Forge: forgehttp.Client{URL: srv.URL, User: admin, PasswordFile: pw}, Org: org, Admin: admin, Tools: tools, KeepMainRules: true,
|
||||
Log: func(format string, a ...any) { said = append(said, fmt.Sprintf(format, a...)) }}, &said
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,9 @@ func runSync() int {
|
||||
toolsAccount = forgehttp.ToolsAccount
|
||||
}
|
||||
s := &Syncer{Forge: forge, Org: "novox", Admin: forge.User, Tools: toolsAccount,
|
||||
Log: func(format string, a ...any) { fmt.Printf(format+"\n", a...) },
|
||||
WaitFor: 3 * time.Minute, Poll: 5 * time.Second}
|
||||
Log: func(format string, a ...any) { fmt.Printf(format+"\n", a...) },
|
||||
KeepMainRules: ReadKeepMainRules(file),
|
||||
WaitFor: 3 * time.Minute, Poll: 5 * time.Second}
|
||||
value, err := ReadValue(file)
|
||||
if err != nil {
|
||||
o := Outcome{At: time.Now().UTC(), Org: s.Org, Failed: err.Error()}
|
||||
|
||||
@@ -58,12 +58,18 @@ func TestManifestDeclaresTheValueAndItsTrigger(t *testing.T) {
|
||||
}
|
||||
filled := filepath.Join(t.TempDir(), "repositories.json")
|
||||
value := `{"mesh-delivery":{"private":false,"description":"d"}}`
|
||||
if err := os.WriteFile(filled, []byte(strings.ReplaceAll(string(text), "${config:"+ConfigKey+"}", value)), 0o600); err != nil {
|
||||
if err := os.WriteFile(filled, []byte(strings.ReplaceAll(strings.ReplaceAll(string(text), "${config:"+ConfigKey+"}", value), "${config:keep-main-rules}", "false")), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v, err := ReadValue(filled); err != nil || len(v) != 1 || v["mesh-delivery"].Description != "d" {
|
||||
t.Errorf("the filled template reads as %v, %v; want mesh-delivery", v, err)
|
||||
}
|
||||
if ReadKeepMainRules(filled) {
|
||||
t.Error("the filled template with keep-main-rules false reads as true")
|
||||
}
|
||||
if k := m.Config["keep-main-rules"]; k == nil || k["type"] != "boolean" || k["default"] != false {
|
||||
t.Errorf("config keep-main-rules = %v, want a boolean defaulting to false", k)
|
||||
}
|
||||
for _, r := range m.Resources {
|
||||
if r["type"] == "process" && has(strs(r["restart-on"]), file["id"].(string)) {
|
||||
process = r
|
||||
|
||||
@@ -103,6 +103,30 @@ type Repo struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// LeftAsSet is what a pass says when keep-main-rules is false.
|
||||
const LeftAsSet = "main's rules are left as set at the forge (keep-main-rules is false): no existing repository's protection was changed"
|
||||
|
||||
// ReadKeepMainRules reads the setting keep-main-rules from the file the mesh renders; absent or not true is false.
|
||||
func ReadKeepMainRules(file string) bool {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var whole struct {
|
||||
Keep any `json:"keep-main-rules"`
|
||||
}
|
||||
if json.Unmarshal(raw, &whole) != nil {
|
||||
return false
|
||||
}
|
||||
switch v := whole.Keep.(type) {
|
||||
case bool:
|
||||
return v
|
||||
case string:
|
||||
return v == "true"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Value is the config value: each repository of the organisation by name.
|
||||
type Value map[string]Repo
|
||||
|
||||
@@ -147,6 +171,8 @@ type Outcome struct {
|
||||
Org string `json:"org"`
|
||||
Failed string `json:"failed,omitempty"` // the pass did not run: why
|
||||
Repositories []Result `json:"repositories"`
|
||||
// MainRules says, when keep-main-rules is false, that no existing repository's protection was changed.
|
||||
MainRules string `json:"main_rules,omitempty"`
|
||||
}
|
||||
|
||||
// Refused names the repositories a step of was refused.
|
||||
@@ -167,7 +193,11 @@ type Syncer struct {
|
||||
Org string // the organisation, novox
|
||||
Admin string // the setup login, under whose own account a repository is made
|
||||
Tools string // the tools' account, the only one that merges into main
|
||||
Log func(format string, a ...any)
|
||||
// KeepMainRules is the setting keep-main-rules: only when true does a pass put right the main rule of a repository
|
||||
// that already exists or swap statuses on one the value does not list. False (the default) makes new repositories
|
||||
// and leaves every existing repository's protection as people set it at the forge.
|
||||
KeepMainRules bool
|
||||
Log func(format string, a ...any)
|
||||
// WaitFor is how long to wait for the forge to answer; none when zero.
|
||||
WaitFor, Poll time.Duration
|
||||
}
|
||||
@@ -199,6 +229,11 @@ func (s *Syncer) Sync(ctx context.Context, v Value) (Outcome, error) {
|
||||
}
|
||||
out.Repositories = append(out.Repositories, r)
|
||||
}
|
||||
if !s.KeepMainRules {
|
||||
out.MainRules = LeftAsSet
|
||||
s.say("%s", LeftAsSet)
|
||||
return out, nil
|
||||
}
|
||||
for _, r := range s.switchUnlisted(ctx, v) {
|
||||
if r.Did == DidRefused {
|
||||
s.say("refused: %s/%s at the step %s: %s", s.Org, r.Name, r.Step, r.Said)
|
||||
@@ -306,6 +341,9 @@ func (s *Syncer) one(ctx context.Context, name string, r Repo) Result {
|
||||
if err != nil {
|
||||
return refused(name, StepRead, err)
|
||||
}
|
||||
if inOrg && !s.KeepMainRules {
|
||||
return Result{Name: name, Did: DidUnchanged}
|
||||
}
|
||||
if inOrg {
|
||||
changed, err := s.putRight(ctx, s.Org, name)
|
||||
if err != nil {
|
||||
|
||||
@@ -478,3 +478,50 @@ func keysOf(m map[string]string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// The operator, 2026-10-23: "disable the gitea branch protection reset thing". With keep-main-rules false (the
|
||||
// default) a pass makes new repositories but writes nothing to any existing repository's protection, listed or not.
|
||||
func TestKeepMainRulesFalseChangesNoExistingProtection(t *testing.T) {
|
||||
f := newFake()
|
||||
hq := f.add("novox/hq", map[string]map[string]any{mainRule: {
|
||||
"enable_push": true, "enable_status_check": true, "status_check_contexts": []any{"mesh/merge-gate"},
|
||||
}}, "main")
|
||||
old := f.add("novox/old", map[string]map[string]any{mainRule: {
|
||||
"enable_push": true, "enable_status_check": true, "status_check_contexts": []any{"mesh/merge-gate"},
|
||||
}}, "main")
|
||||
s, said := syncer(t, f)
|
||||
s.KeepMainRules = false
|
||||
o := run(t, s, Value{"hq": {}, "fresh": {}})
|
||||
|
||||
for _, c := range f.writes() {
|
||||
if strings.Contains(c, "/novox/hq") || strings.Contains(c, "/novox/old") || strings.Contains(c, "/repos/novox/hq") || strings.Contains(c, "/repos/novox/old") {
|
||||
t.Errorf("an existing repository was written: %s", c)
|
||||
}
|
||||
}
|
||||
if hq.Rules[mainRule]["enable_push"] != true || old.Rules[mainRule]["enable_push"] != true {
|
||||
t.Error("an existing repository's rule was changed")
|
||||
}
|
||||
if f.repos["novox/fresh"] == nil {
|
||||
t.Error("the new repository was not made")
|
||||
}
|
||||
if r := result(o, "hq"); r.Did != DidUnchanged {
|
||||
t.Errorf("hq outcome %+v", r)
|
||||
}
|
||||
if o.MainRules != LeftAsSet || !strings.Contains(strings.Join(*said, "\n"), "keep-main-rules is false") {
|
||||
t.Errorf("the outcome does not say the rules were left: %q", o.MainRules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadKeepMainRules(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for body, want := range map[string]bool{`{}`: false, `{"keep-main-rules": false}`: false, `{"keep-main-rules": true}`: true, `{"keep-main-rules": "true"}`: true, `not json`: false} {
|
||||
f := filepath.Join(dir, "v.json")
|
||||
_ = os.WriteFile(f, []byte(body), 0o644)
|
||||
if got := ReadKeepMainRules(f); got != want {
|
||||
t.Errorf("%s: got %v, want %v", body, got, want)
|
||||
}
|
||||
}
|
||||
if ReadKeepMainRules(filepath.Join(dir, "missing")) {
|
||||
t.Error("a missing file must be false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"repositories": ${config:repositories}}
|
||||
{"repositories": ${config:repositories}, "keep-main-rules": ${config:keep-main-rules}}
|
||||
|
||||
@@ -51,6 +51,7 @@ import { state } from "@novox/mesh-sdk/state";
|
||||
import { announce, keepWatermark, readWatermark } from "./announce.js";
|
||||
import { mergeTool } from "./merge.js";
|
||||
import { GiteaAdmin, GiteaClient, movedSince } from "./client.js";
|
||||
import { keepMainRules, mainRulesStep } from "./mainrules.js";
|
||||
import { CHECK_CONTEXT, CHECK_STATUS, REPO_CHECK_CONTEXT, commentFor, composedGate, groupGateStatus, headsToAnnounce, maintained,
|
||||
requiredOf, statusesFor, writesGate, type Announced, type Checked, repoCheckFacts, reposToLook } from "./pulls.js";
|
||||
import { GateWriter, clonedAs, type BuildStarted } from "./gate.js";
|
||||
@@ -578,7 +579,6 @@ async function listenForForge(client: GiteaClient): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
let mainTouched = -1;
|
||||
if (gitea) {
|
||||
const client = gitea;
|
||||
// A poll that fails says so once, not once a minute: the same reason repeating (the forge not up
|
||||
@@ -608,11 +608,8 @@ if (gitea) {
|
||||
await listenForForge(client);
|
||||
// Main takes no push and only the tools' account merges, in every repository, kept and not only when a token is
|
||||
// minted: at the start and every ten minutes after, so a repository made later gets it too (issue 948, ADR 0398 §2).
|
||||
tick(async () => {
|
||||
const touched = await GiteaAdmin.fromEnv().ensureMainMerges("novox");
|
||||
if (touched.length !== mainTouched) console.log(`[gitea] main's rule is right in ${touched.length} repositories`);
|
||||
mainTouched = touched.length;
|
||||
}, 600_000);
|
||||
// Only while the setting keep-main-rules is true (mainrules.ts); false, the default, leaves every repository's rule as set.
|
||||
tick(mainRulesStep(keepMainRules(), () => GiteaAdmin.fromEnv().ensureMainMerges("novox"), (line) => console.log(line)), 600_000);
|
||||
tick(() => pollRepos(client), 60_000);
|
||||
tick(() => pollMerged(client), () => pace.every());
|
||||
tick(() => pollPulls(client), () => pace.every());
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// The setting keep-main-rules (the operator, 2026-10-23: "disable the gitea branch protection reset thing"): only when it
|
||||
// is true does the module put main's rule right in every repository of the organisation (client.ts ensureMainMerges).
|
||||
// False, the default, leaves every repository's branch protection as people set it at the forge.
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
export const LEFT_AS_SET = "[gitea] main's rules are left as set at the forge (keep-main-rules is false)";
|
||||
|
||||
/** The setting from the settings-merged config file the mesh delivers; absent, unreadable or not true is false. */
|
||||
export function keepMainRules(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const file = env.MESH_GITEA_CONFIG_FILE;
|
||||
if (!file) return false;
|
||||
try {
|
||||
const v = (JSON.parse(readFileSync(file, "utf8")) as Record<string, unknown>)["keep-main-rules"];
|
||||
return v === true || v === "true";
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
/** The 10-minute step: puts the rules right only when kept; else says once that they are left and writes nothing. */
|
||||
export function mainRulesStep(keep: boolean, put: () => Promise<string[]>, say: (line: string) => void): () => Promise<void> {
|
||||
let said = false;
|
||||
let touched = -1;
|
||||
return async () => {
|
||||
if (!keep) {
|
||||
if (!said) say(LEFT_AS_SET);
|
||||
said = true;
|
||||
return;
|
||||
}
|
||||
const repos = await put();
|
||||
if (repos.length !== touched) say(`[gitea] main's rule is right in ${repos.length} repositories`);
|
||||
touched = repos.length;
|
||||
};
|
||||
}
|
||||
@@ -170,6 +170,12 @@
|
||||
"trusted": true,
|
||||
"default": {}
|
||||
},
|
||||
"keep-main-rules": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the module keeps main's branch protection as the mesh wants it. True: every ten minutes, and at the start, main's rule in every repository of the organisation novox is put to no push and the tools' account alone merging, and each apply of the `repositories` value narrows the main rule of the repositories it lists and swaps mesh/merge-gate for mesh/check in those it does not. False (the default): the module changes no existing repository's branch protection, whoever set it at the forge; a repository listed in `repositories` that does not exist is still made, with its first protection.",
|
||||
"trusted": true,
|
||||
"default": false
|
||||
},
|
||||
"repositories": {
|
||||
"type": "object",
|
||||
"description": "The repositories of the organisation novox the mesh holds (ADR 0428): each by name, {\"<name>\": {\"private\": true, \"description\": \"...\"}}. One that does not exist is made by the module's setup login under its own account, its main protected and given a first commit (a README naming the description and a merge-check.sh), its rule narrowed to no push and the tools' merge requiring mesh/check, and only then moved into the organisation; one that exists only has main's rule put right, mesh/check in place of mesh/merge-gate and mesh/repo-check and its other statuses kept (a main requiring mesh/repo-check alone keeps it). Every other repository whose main requires mesh/merge-gate has the two swapped for mesh/check; nothing else of a repository out of the value is touched.",
|
||||
@@ -303,7 +309,8 @@
|
||||
"template": "files/config.json",
|
||||
"merge": "json",
|
||||
"takes-config": [
|
||||
"url"
|
||||
"url",
|
||||
"keep-main-rules"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// The operator, 2026-10-23: "disable the gitea branch protection reset thing". keep-main-rules false (the default)
|
||||
// writes nothing at the forge, true puts main's rule right as before.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { keepMainRules, mainRulesStep, LEFT_AS_SET } from "../dist/mainrules.js";
|
||||
|
||||
function env(body?: string): NodeJS.ProcessEnv {
|
||||
if (body === undefined) return {};
|
||||
const f = join(mkdtempSync(join(tmpdir(), "gitea-keep-")), "config.json");
|
||||
writeFileSync(f, body);
|
||||
return { MESH_GITEA_CONFIG_FILE: f };
|
||||
}
|
||||
|
||||
test("the setting is false unless the config says true", () => {
|
||||
assert.equal(keepMainRules(env()), false);
|
||||
assert.equal(keepMainRules(env("{}")), false);
|
||||
assert.equal(keepMainRules(env('{"keep-main-rules":false}')), false);
|
||||
assert.equal(keepMainRules(env("not json")), false);
|
||||
assert.equal(keepMainRules(env('{"keep-main-rules":true}')), true);
|
||||
});
|
||||
|
||||
test("false: the step never writes and says once that the rules are left", async () => {
|
||||
let writes = 0;
|
||||
const said: string[] = [];
|
||||
const step = mainRulesStep(false, async () => { writes++; return ["a"]; }, (l) => said.push(l));
|
||||
await step(); await step(); await step();
|
||||
assert.equal(writes, 0);
|
||||
assert.deepEqual(said, [LEFT_AS_SET]);
|
||||
assert.match(LEFT_AS_SET, /main's rules are left as set at the forge \(keep-main-rules is false\)/);
|
||||
});
|
||||
|
||||
test("true: every step puts the rules right", async () => {
|
||||
let writes = 0;
|
||||
const said: string[] = [];
|
||||
const step = mainRulesStep(true, async () => { writes++; return ["a", "b"]; }, (l) => said.push(l));
|
||||
await step(); await step();
|
||||
assert.equal(writes, 2);
|
||||
assert.deepEqual(said, ["[gitea] main's rule is right in 2 repositories"]);
|
||||
});
|
||||
|
||||
test("a token mint's account setup writes no protection while keep-main-rules is false, and does when true", async () => {
|
||||
const { setUpToolsAccount } = await import("../dist/tools-account.js");
|
||||
for (const [keep, want] of [[false, 0], [true, 1]] as const) {
|
||||
let merges = 0;
|
||||
const forge = {
|
||||
ensureUser: async () => {}, ensureTeamWith: async () => 1, addUserToTeam: async () => {},
|
||||
ensureMainMerges: async () => { merges++; return []; },
|
||||
};
|
||||
await setUpToolsAccount(forge, "mesh-tools", () => "pw", keep);
|
||||
assert.equal(merges, want);
|
||||
}
|
||||
// The default comes from the setting, which is false when the config does not say true.
|
||||
let merges = 0;
|
||||
const saved = process.env.MESH_GITEA_CONFIG_FILE;
|
||||
delete process.env.MESH_GITEA_CONFIG_FILE;
|
||||
try {
|
||||
await setUpToolsAccount({ ensureUser: async () => {}, ensureTeamWith: async () => 1, addUserToTeam: async () => {}, ensureMainMerges: async () => { merges++; return []; } }, "mesh-tools", () => "pw");
|
||||
} finally { if (saved !== undefined) process.env.MESH_GITEA_CONFIG_FILE = saved; }
|
||||
assert.equal(merges, 0);
|
||||
});
|
||||
@@ -192,11 +192,14 @@ function fakeForge() {
|
||||
const forge = await fakeForge();
|
||||
after(() => forge.close());
|
||||
|
||||
async function environment(): Promise<NodeJS.ProcessEnv & { MESH_GITEA_STATE_DIR: string }> {
|
||||
async function environment(keepMainRules = false): Promise<NodeJS.ProcessEnv & { MESH_GITEA_STATE_DIR: string }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "gitea-tools-account-"));
|
||||
const passwordFile = join(dir, "admin.secret");
|
||||
await writeFile(passwordFile, PASSWORD + "\n", { mode: 0o600 });
|
||||
const config = join(dir, "config.json");
|
||||
await writeFile(config, JSON.stringify({ "keep-main-rules": keepMainRules }));
|
||||
return {
|
||||
MESH_GITEA_CONFIG_FILE: config,
|
||||
MESH_GITEA_URL: forge.url,
|
||||
MESH_GITEA_ADMIN_USER: ADMIN,
|
||||
MESH_GITEA_ADMIN_PASSWORD_FILE: passwordFile,
|
||||
@@ -351,7 +354,7 @@ test("a team another process made between the look and the create is found, not
|
||||
});
|
||||
|
||||
test("setting the tools' account up puts main's rule right in every repository: no push, only the tools' account merges (issue 948)", async () => {
|
||||
const env = await environment();
|
||||
const env = await environment(true);
|
||||
const from = forge.calls.length;
|
||||
await fresh(env).listRepos();
|
||||
const hq = forge.mainRules.get("hq");
|
||||
@@ -369,7 +372,7 @@ test("setting the tools' account up puts main's rule right in every repository:
|
||||
});
|
||||
|
||||
test("a glob rule covering main keeps its statuses when main is given a rule of its own name (issue 948, review round 1)", async () => {
|
||||
const env = await environment();
|
||||
const env = await environment(true);
|
||||
await fresh(env).listRepos();
|
||||
const rule = forge.mainRules.get("globbed");
|
||||
assert.ok(rule, "main is given its own rule");
|
||||
@@ -390,3 +393,11 @@ test("a start with the token kept still puts main's rules right: the module's st
|
||||
await GiteaAdmin.fromEnv(env).ensureMainMerges("novox");
|
||||
assert.equal(forge.mainRules.get("hq").enable_push, false, "a rule gone is made again with no token minted");
|
||||
});
|
||||
|
||||
test("a mint with keep-main-rules false (the default) writes no branch protection", async () => {
|
||||
const env = await environment();
|
||||
const from = forge.calls.length;
|
||||
await fresh(env).listRepos();
|
||||
const writes = forge.calls.slice(from).filter((c: string) => /branch_protections/.test(c) && !/^GET /.test(c) && !/^(GET|HEAD) /.test(c));
|
||||
assert.deepEqual(writes, []);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,8 @@ import { dirname, join } from "node:path";
|
||||
import { setUpToolsAccount, TOOLS_ACCOUNT, type AdminCall } from "./tools-account.js";
|
||||
|
||||
/** The name the token carries in the forge's own list — one per mesh runtime, found by name. */
|
||||
import { keepMainRules } from "./mainrules.js";
|
||||
|
||||
export const TOKEN_NAME = "mesh-tools";
|
||||
|
||||
/**
|
||||
@@ -181,7 +183,7 @@ export class MintedToken implements TokenSource {
|
||||
// GiteaAdmin is loaded when first used: client.ts imports this file.
|
||||
setUp: async (password) => {
|
||||
const { GiteaAdmin } = await import("./client.js");
|
||||
await setUpToolsAccount(new GiteaAdmin(url, admin!, password), account, () => randomBytes(24).toString("base64url"));
|
||||
await setUpToolsAccount(new GiteaAdmin(url, admin!, password), account, () => randomBytes(24).toString("base64url"), keepMainRules(env));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
// or a membership lost behind the module's back is made again at the next mint.
|
||||
|
||||
/** The account the tools act as. */
|
||||
import { keepMainRules } from "./mainrules.js";
|
||||
|
||||
export const TOOLS_ACCOUNT = "mesh-tools";
|
||||
/** The organisation whose repositories are the mesh's, and the team through which the tools' account writes there. */
|
||||
export const TOOLS_ORG = "novox";
|
||||
@@ -47,11 +49,12 @@ export interface ToolsForge {
|
||||
|
||||
/** Make the tools' account, plain, a member of its team on every repository of the organisation. Its password is
|
||||
* set afresh each time and kept nowhere: the account is reached only by the tokens the administrator mints. */
|
||||
export async function setUpToolsAccount(forge: ToolsForge, account = TOOLS_ACCOUNT, password: () => string): Promise<void> {
|
||||
export async function setUpToolsAccount(forge: ToolsForge, account = TOOLS_ACCOUNT, password: () => string, keepRules: boolean = keepMainRules()): Promise<void> {
|
||||
await forge.ensureUser(account, password(), `${account}@noreply.localhost`, true);
|
||||
const team = await forge.ensureTeamWith(TOOLS_ORG, TOOLS_TEAM, { name: TOOLS_TEAM, ...TOOLS_TEAM_UNITS });
|
||||
await forge.addUserToTeam(team, account);
|
||||
// Main takes no push and only this account merges into it (novox/hq ADR 0398 §2, issue 948): put right in every
|
||||
// repository now, by the administrator's login, which the account itself is not.
|
||||
await forge.ensureMainMerges?.(TOOLS_ORG);
|
||||
// Only while the setting keep-main-rules is true (mainrules.ts; the operator, 2026-10-23); else no protection is written.
|
||||
if (keepRules) await forge.ensureMainMerges?.(TOOLS_ORG);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user