Skip to content

feat!: split startercode policy into branches and issues; apply branch rules during generate#58

Merged
obcode merged 1 commit into
mainfrom
feature/branch_rules
Apr 23, 2026
Merged

feat!: split startercode policy into branches and issues; apply branch rules during generate#58
obcode merged 1 commit into
mainfrom
feature/branch_rules

Conversation

@obcode

@obcode obcode commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add explicit branches and issues config blocks, independent from startercode
  • Keep startercode focused on source/target branch mapping
  • Preserve startercode.additionalBranches mirror behavior (starter/x -> repo/x)
  • Apply configured branch rules during generate (not only via protect)
  • Improve show output formatting and include startercode.additionalBranches
  • Update docs with migration guide and clear branch/base-branch semantics
  • Add and adapt tests for config parsing, branch protection flow, and show output

Breaking Changes

  • Branch and issue policy moved out of startercode into branches and issues
  • Legacy startercode.* policy keys are now compatibility fallback and should be migrated
  • Branches not listed in startercode.additionalBranches are initialized from toBranch state

Migration Notes

  • Keep in startercode: url, fromBranch, toBranch, additionalBranches
  • Move branch behavior (protect, mergeOnly, default) to branches
  • Move issue replication to issues.replicateFromStartercode / issues.issueNumbers
  • Use glabs protect to apply branch rules to already existing repositories

BREAKING CHANGE: branch and issue policy moved from startercode to branches/issues.

- Migrate legacy `devBranch`, `protectToBranch`, and `protectDevBranchMergeOnly` keys to a new `branches` block for better clarity and separation of concerns.
- Introduce `issues` block for issue replication settings, moving from `startercode`.
- Update documentation to reflect new configuration structure and provide migration guidance.
- Implement branch protection and default settings directly within the `branches` configuration.
- Adjust related code in GitLab client to accommodate new structure, ensuring backward compatibility with legacy keys.
- Enhance tests to validate new branch protection logic and configuration handling.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 23, 2026 21:04
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

github.com/obcode/glabs/cmd/archive.go:12:		init				100.0%
github.com/obcode/glabs/cmd/check.go:10:		init				100.0%
github.com/obcode/glabs/cmd/clone.go:45:		init				100.0%
github.com/obcode/glabs/cmd/delete.go:12:		init				100.0%
github.com/obcode/glabs/cmd/generate.go:12:		init				100.0%
github.com/obcode/glabs/cmd/protect.go:12:		init				100.0%
github.com/obcode/glabs/cmd/report.go:14:		init				100.0%
github.com/obcode/glabs/cmd/root.go:39:			Execute				0.0%
github.com/obcode/glabs/cmd/root.go:43:			init				100.0%
github.com/obcode/glabs/cmd/root.go:51:			er				0.0%
github.com/obcode/glabs/cmd/root.go:56:			initConfig			0.0%
github.com/obcode/glabs/cmd/setaccess.go:12:		init				100.0%
github.com/obcode/glabs/cmd/show.go:8:			init				100.0%
github.com/obcode/glabs/cmd/update.go:12:		init				100.0%
github.com/obcode/glabs/cmd/urls.go:22:			init				100.0%
github.com/obcode/glabs/cmd/version.go:10:		init				100.0%
github.com/obcode/glabs/config/assignment.go:11:	String				100.0%
github.com/obcode/glabs/config/assignment.go:24:	GetAssignmentConfig		88.2%
github.com/obcode/glabs/config/assignment.go:80:	RepoSuffix			100.0%
github.com/obcode/glabs/config/assignment.go:94:	RepoBaseName			100.0%
github.com/obcode/glabs/config/assignment.go:102:	RepoNameWithSuffix		100.0%
github.com/obcode/glabs/config/assignment.go:106:	RepoNameForStudent		100.0%
github.com/obcode/glabs/config/assignment.go:110:	RepoNameForGroup		100.0%
github.com/obcode/glabs/config/assignment.go:114:	assignmentPath			100.0%
github.com/obcode/glabs/config/assignment.go:128:	per				100.0%
github.com/obcode/glabs/config/assignment.go:135:	description			100.0%
github.com/obcode/glabs/config/assignment.go:145:	mergeRequest			83.3%
github.com/obcode/glabs/config/course.go:14:		GetCourseConfig			50.0%
github.com/obcode/glabs/config/release.go:8:		release				100.0%
github.com/obcode/glabs/config/release.go:22:		releaseMergeRequest		100.0%
github.com/obcode/glabs/config/release.go:46:		dockerImages			100.0%
github.com/obcode/glabs/config/repo.go:10:		startercode			87.5%
github.com/obcode/glabs/config/repo.go:44:		branches			90.2%
github.com/obcode/glabs/config/repo.go:114:		defaultBranch			37.5%
github.com/obcode/glabs/config/repo.go:129:		issues				100.0%
github.com/obcode/glabs/config/repo.go:150:		clone				100.0%
github.com/obcode/glabs/config/repo.go:172:		SetBranch			100.0%
github.com/obcode/glabs/config/repo.go:176:		SetProtectToBranch		88.9%
github.com/obcode/glabs/config/repo.go:194:		SetLocalpath			100.0%
github.com/obcode/glabs/config/repo.go:198:		SetForce			100.0%
github.com/obcode/glabs/config/seeder.go:15:		seeder				53.8%
github.com/obcode/glabs/config/show.go:10:		Show				98.0%
github.com/obcode/glabs/config/students.go:14:		SetAccessLevel			100.0%
github.com/obcode/glabs/config/students.go:28:		accessLevel			100.0%
github.com/obcode/glabs/config/students.go:43:		students			100.0%
github.com/obcode/glabs/config/students.go:75:		mkStudents			94.4%
github.com/obcode/glabs/config/students.go:108:		groups				100.0%
github.com/obcode/glabs/config/urls.go:5:		Urls				100.0%
github.com/obcode/glabs/git/auth.go:11:			GetAuth				90.0%
github.com/obcode/glabs/git/clone.go:18:		Clone				80.0%
github.com/obcode/glabs/git/clone.go:38:		cloneurl			100.0%
github.com/obcode/glabs/git/clone.go:44:		localpath			100.0%
github.com/obcode/glabs/git/clone.go:48:		clone				36.1%
github.com/obcode/glabs/git/starterrepo.go:22:		PrepareStartercodeRepo		0.0%
github.com/obcode/glabs/gitlab/archive.go:14:		Archive				77.8%
github.com/obcode/glabs/gitlab/archive.go:32:		archivePerStudent		90.9%
github.com/obcode/glabs/gitlab/archive.go:54:		archivePerGroup			90.9%
github.com/obcode/glabs/gitlab/archive.go:76:		archive				75.6%
github.com/obcode/glabs/gitlab/branches.go:12:		syncConfiguredBranches		0.0%
github.com/obcode/glabs/gitlab/branches.go:49:		defaultBranchName		0.0%
github.com/obcode/glabs/gitlab/branches.go:67:		isBranchAlreadyExistsError	0.0%
github.com/obcode/glabs/gitlab/check.go:9:		CheckCourse			90.6%
github.com/obcode/glabs/gitlab/check.go:67:		checkStudent			100.0%
github.com/obcode/glabs/gitlab/check.go:89:		checkDupsInGroups		100.0%
github.com/obcode/glabs/gitlab/delete.go:11:		Delete				77.8%
github.com/obcode/glabs/gitlab/delete.go:29:		deletePerStudent		100.0%
github.com/obcode/glabs/gitlab/delete.go:40:		deletePerGroup			100.0%
github.com/obcode/glabs/gitlab/delete.go:51:		delete				92.9%
github.com/obcode/glabs/gitlab/generate.go:14:		Generate			38.9%
github.com/obcode/glabs/gitlab/generate.go:51:		generate			0.0%
github.com/obcode/glabs/gitlab/generate.go:226:		generatePerStudent		0.0%
github.com/obcode/glabs/gitlab/generate.go:239:		generatePerGroup		0.0%
github.com/obcode/glabs/gitlab/gitlab.go:13:		NewClient			80.0%
github.com/obcode/glabs/gitlab/groups.go:12:		getGroupIDByFullPath		100.0%
github.com/obcode/glabs/gitlab/groups.go:33:		getGroupID			100.0%
github.com/obcode/glabs/gitlab/groups.go:46:		createGroup			83.3%
github.com/obcode/glabs/gitlab/issues.go:14:		getStartercodeProject		100.0%
github.com/obcode/glabs/gitlab/issues.go:58:		replicateIssue			100.0%
github.com/obcode/glabs/gitlab/projects.go:12:		generateProject			77.5%
github.com/obcode/glabs/gitlab/projects.go:97:		getProjectByName		80.0%
github.com/obcode/glabs/gitlab/protect.go:15:		ProtectToBranch			77.8%
github.com/obcode/glabs/gitlab/protect.go:33:		protectBranch			75.0%
github.com/obcode/glabs/gitlab/protect.go:104:		hasProtectedBranches		100.0%
github.com/obcode/glabs/gitlab/protect.go:114:		protectSingleBranch		76.5%
github.com/obcode/glabs/gitlab/protect.go:170:		replaceBranchPermissions	88.9%
github.com/obcode/glabs/gitlab/protect.go:186:		isProtectedBranchNotFoundError	75.0%
github.com/obcode/glabs/gitlab/protect.go:195:		protectToBranchPerStudent	90.9%
github.com/obcode/glabs/gitlab/protect.go:217:		protectToBranchPerGroup		72.7%
github.com/obcode/glabs/gitlab/report.go:14:		Report				78.9%
github.com/obcode/glabs/gitlab/report.go:51:		ReportHTML			73.7%
github.com/obcode/glabs/gitlab/report.go:86:		ReportJSON			77.8%
github.com/obcode/glabs/gitlab/report_helper.go:17:	report				82.5%
github.com/obcode/glabs/gitlab/report_helper.go:135:	projectReport			85.9%
github.com/obcode/glabs/gitlab/seeder.go:21:		localpath			0.0%
github.com/obcode/glabs/gitlab/seeder.go:25:		runSeeder			0.0%
github.com/obcode/glabs/gitlab/seeder.go:100:		addAndCommit			0.0%
github.com/obcode/glabs/gitlab/seeder.go:121:		push				0.0%
github.com/obcode/glabs/gitlab/setaccess.go:14:		Setaccess			77.8%
github.com/obcode/glabs/gitlab/setaccess.go:32:		setaccess			41.5%
github.com/obcode/glabs/gitlab/setaccess.go:140:	inviteByEmail			100.0%
github.com/obcode/glabs/gitlab/setaccess.go:155:	setaccessPerStudent		100.0%
github.com/obcode/glabs/gitlab/setaccess.go:175:	setaccessPerGroup		80.0%
github.com/obcode/glabs/gitlab/starterrepo.go:14:	pushStartercode			0.0%
github.com/obcode/glabs/gitlab/update.go:15:		Update				60.0%
github.com/obcode/glabs/gitlab/update.go:44:		update				31.6%
github.com/obcode/glabs/gitlab/update.go:91:		updatePerStudent		100.0%
github.com/obcode/glabs/gitlab/update.go:111:		updatePerGroup			100.0%
github.com/obcode/glabs/gitlab/users.go:12:		getUser				95.0%
github.com/obcode/glabs/gitlab/users.go:49:		getUserID			100.0%
github.com/obcode/glabs/gitlab/users.go:63:		addMember			94.1%
github.com/obcode/glabs/main.go:18:			main				0.0%
total:							(statements)			66.8%

@obcode obcode merged commit a1aba84 into main Apr 23, 2026
12 checks passed
@obcode obcode deleted the feature/branch_rules branch April 23, 2026 21:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors assignment policy configuration by splitting legacy startercode responsibilities into dedicated branches and issues blocks, and applies branch rules automatically during repository generation (not only via protect). It also updates CLI show output formatting and provides documentation + migration guidance for the breaking config changes.

Changes:

  • Introduce branches (creation/default/protection/merge-only) and issues (replication) config blocks, with legacy startercode.* keys supported as fallback.
  • Apply branch creation/default/protection rules during generate via a new syncConfiguredBranches flow.
  • Update tests and documentation (including a new migration guide) and improve show output (including startercode.additionalBranches).

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
gitlab/starterrepo.go Removes dev-branch/protect behavior from startercode push; tweaks additional-branch push logging/behavior.
gitlab/runtime_test.go Updates runtime test config to use new Branches rules.
gitlab/protect.go Migrates branch protection logic to Branches rules; adds protected-branch update flow.
gitlab/protect_contract_test.go Adapts contract tests to new protection flow (GET+PATCH vs DELETE+POST).
gitlab/integration_gitlab_test.go Updates integration test setup to use Branches configuration.
gitlab/generate.go Applies configured branch rules during generate; switches issue replication to Issues block.
gitlab/branches.go Adds branch sync helper to create branches, set default branch, and invoke protection.
docs/workflows.md Updates workflow examples to use branches / issues.
docs/troubleshooting.md Updates troubleshooting guidance for merge-only behavior under branches.
docs/migration.md Adds migration guide from legacy startercode policy keys to branches / issues.
docs/getting-started.md Updates configuration defaults table and links migration guide.
docs/configuration.md Documents new branches/issues options and clarifies base-branch semantics.
docs/advanced.md Updates advanced example configuration to use branches.
config/urls_show_test.go Adds/updates tests for Show() output (branches, issues, additionalBranches, fmt artifacts).
config/types.go Introduces BranchRule and IssueReplication types; trims Startercode to mapping-only fields.
config/show.go Rewrites Show() output formatting and adds Branches/Issues sections.
config/setters_test.go Updates setter tests to validate SetProtectToBranch populates Branches.
config/repo.go Implements parsing/legacy fallback for branches and issues; makes clone default branch depend on configured default branch.
config/repo_test.go Adds tests for new parsing behavior, legacy fallback, and clone default-branch behavior.
config/assignment.go Wires startercode/branches/issues parsing into GetAssignmentConfig and sets clone default branch accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gitlab/starterrepo.go
Comment on lines 55 to +82
for _, additionalBranch := range assignmentCfg.Startercode.AdditionalBranches {
log.Debug().Str("branch", additionalBranch).Msg("pushing additional branch")

// worktree, err := from.Repo.Worktree()
// if err != nil {
// log.Debug().Err(err).
// Str("branch", additionalBranch).
// Str("name", project.Name).Str("url", project.SSHURLToRepo).
// Msg("cannot get worktree")
// return fmt.Errorf("cannot get worktree: %w", err)
// }

// worktree.Checkout(&git.CheckoutOptions{
// Branch: plumbing.ReferenceName(additionalBranch),
// })
if additionalBranch == "" {
continue
}

refSpec := config.RefSpec(fmt.Sprintf("+refs/remotes/origin/%s:refs/heads/%s", additionalBranch, additionalBranch))

log.Debug().
Str("refSpec", string(refSpec)).
Str("name", project.Name).
Str("toURL", project.SSHURLToRepo).
Str("branch", additionalBranch).
Msg("pushing additional startercode branch")

pushOpts := &git.PushOptions{
RemoteName: remote.Config().Name,
RefSpecs: []config.RefSpec{refSpec},
Auth: from.Auth,
}
err = from.Repo.Push(pushOpts)
if err != nil {
log.Debug().Err(err).
log.Warn().Err(err).
Str("branch", additionalBranch).
Str("refspec", refSpec.String()).
Str("name", project.Name).Str("url", project.SSHURLToRepo).
Msg("cannot push to remote")
return fmt.Errorf("cannot push to remote: %w", err)
Str("refSpec", refSpec.String()).
Str("name", project.Name).
Str("url", project.SSHURLToRepo).
Msg("cannot push additional branch to remote, continuing with other setup steps")
continue

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing an explicitly configured startercode.additionalBranches branch is now best-effort (Warn + continue). If the push fails (e.g., branch missing in starter), generation continues and later branch creation may fall back to baseBranch content, leaving a branch that exists but has the wrong history/content. Consider failing the generate step (or at least returning an aggregated error) when a configured additional branch cannot be mirrored, so misconfigurations don’t silently produce incorrect repositories.

Copilot uses AI. Check for mistakes.
Comment thread gitlab/generate.go
Comment on lines +164 to +175
if generated && len(assignmentCfg.Branches) > 0 {
baseBranch := defaultBranchName(assignmentCfg.Branches, "main")
if assignmentCfg.Startercode != nil {
baseBranch = assignmentCfg.Startercode.ToBranch
} else if assignmentCfg.Seeder != nil {
baseBranch = assignmentCfg.Seeder.ToBranch
}

if err := c.syncConfiguredBranches(assignmentCfg, project, baseBranch); err != nil {
log.Debug().Err(err).Str("project", project.Name).Msg("cannot apply configured branch rules")
}
}

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syncConfiguredBranches is called for any generated project with a non-empty Branches config, even when neither startercode nor seeder ran. Newly created GitLab projects are empty by default (no ref), so creating branches from baseBranch and setting DefaultBranch will fail because baseBranch doesn’t exist yet. Consider guarding this block to only run after startercode/seeder created the base branch, or initialize the project with an initial commit/README before applying branch rules.

Copilot uses AI. Check for mistakes.
Comment thread gitlab/protect.go
Comment on lines +186 to +193
func isProtectedBranchNotFoundError(err error) bool {
if err == nil {
return false
}

msg := strings.ToLower(err.Error())
return strings.Contains(msg, "404") || strings.Contains(msg, "not found")
}

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isProtectedBranchNotFoundError detects 404s by substring matching on err.Error(). This is brittle and can misclassify unrelated errors (different wording/localization, or other statuses containing “not found”). Prefer checking the GitLab API response status code (the *Response returned by GetProtectedBranch) or using errors.As to inspect a structured error type, and only treating an actual 404 as the “not protected yet” case.

Copilot uses AI. Check for mistakes.
Comment thread gitlab/branches.go
Comment on lines +67 to +70
func isBranchAlreadyExistsError(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "already exists") || strings.Contains(msg, "has already been taken")
}

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isBranchAlreadyExistsError relies on substring matching against err.Error() to detect the “branch already exists” case. This is fragile (message text can change) and risks either masking real errors or failing to ignore the specific “already exists” condition. Consider using the GitLab API response/status code (e.g., 400 with the known validation error) or a structured error type from the client library instead of parsing strings.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants