Skip to content

refactor(config): return errors instead of exiting the process#75

Merged
obcode merged 1 commit into
mainfrom
refactor/config-errors
Jul 17, 2026
Merged

refactor(config): return errors instead of exiting the process#75
obcode merged 1 commit into
mainfrom
refactor/config-errors

Conversation

@obcode

@obcode obcode commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Schritt 1 des glabs-web-Plans. Ersetzt #74, das GitHub automatisch schloss, als der Base-Branch beim Merge von #73 gelöscht wurde. Inhalt unverändert, auf main rebast.

Warum

config/ rief bei jedem Fehler log.Fatal (= os.Exit(1)). Damit ist das Paket ausschließlich aus einem CLI heraus benutzbar: ein Bibliotheks- oder Server-Konsument würde von einem Config-Tippfehler mitten im Request abgeschossen. Die 17 Exit-Stellen werden zu error, durchgereicht über GetAssignmentConfig/GetCourseConfig. cmd/ ist danach die einzige Schicht, die den Prozess beendet.

Verhaltenserhaltend — und zwar nachgewiesen

  • Die Goldens aus test(config): pin resolved config with golden tests #73 laufen unverändert durch. Genau dafür waren sie da: das hier ist eine Signaturänderung, keine Verhaltensänderung.
  • Unabhängige Gegenprobe: glabs show für alle 13 Assignments aus mpd/vss gegen ein Binary vom Stand vor dem Umbau gediffed → identisch. Show() rendert anders als der Golden-View, insofern prüft das eine andere Seite.

Fußnote dazu: der Vergleich braucht sortierten Output. config.Show() gibt deferredBranches direkt aus einer Go-Map aus, die Reihenfolge variiert also schon heute zwischen zwei Läufen desselben Binaries. Kosmetisch, vorbestehend, hier bewusst nicht angefasst (Show() wird in Schritt 5 ohnehin auf den Reporter umgebaut). Die Goldens sind davon nicht betroffen, weil encoding/json Map-Keys sortiert.

Der eigentliche Gewinn: Fehlerpfade sind erstmals testbar

config/errors_test.go deckt alle 15 ab — unbekannter Kurs/Assignment, abstraktes Assignment direkt benutzt, extends-Zyklus (über zwei Assignments und auf sich selbst), extends kein String / leer / Parent fehlt, startercode ohne url, seeder ohne cmd, kaputter signKey, Legacy-users-Key, ungültiges whenCommitAdded. Jeder einzelne davon hätte vorher den Testprozess gekillt.

Das neue testdata/errors/edge.yaml liegt bewusst außerhalb von testdata/courses/, damit der Golden-Test nicht versucht, Fixtures aufzulösen, die scheitern sollen.

Fund: die zwei nachrichtenlosen log.Fatal() in seeder.go waren schlimmer als gedacht

Ich hatte sie beim Planen als „exit mit leerer Logzeile" notiert. Falsch: zerolog löst den Exit erst in Msg()/Send() aus. Ein log.Fatal() ohne Message loggt also nichts und beendet nichts — die Ausführung lief durch auf entities[0] eines leeren Slices und panickte mit index out of range. Empirisch verifiziert.

Beide sind jetzt echte Fehler, plus die fehlenden len(entities)- und PrivateKey == nil-Guards. Latent (kein echtes YAML nutzt seeder.signKey), aber der Fehlerpfad ist jetzt einer.

Bewusste Abweichung vom Plan: das viper.Set bleibt

Der Plan sah vor, den Write-Back aus dem Lesepfad (config/inheritance.go:54) hier zu entfernen. Das hieße, Zugriff auf die gemergte Map durch ~63 Reader zu fädeln — exakt die Arbeit, die Schritt 2 macht und die Schritt 2 dann sofort wieder wegwirft, weil Resolve() dort per Konstruktion rein wird. Der Zwischenschritt wäre zu 100% Wegwerfarbeit.

Es verschwindet in Schritt 2 gratis. Bis dahin ist es an Ort und Stelle dokumentiert, inklusive der Konsequenz (nicht nebenläufigkeitssicher, Aufrufer müssen ein Assignment nach dem anderen auflösen).

Damit stimmt auch die Plan-Behauptung „config/-Tests brauchen kein viper.Reset() mehr" für diesen Schritt noch nicht — die gilt erst nach Schritt 2, wenn config/ den viper-Global gar nicht mehr liest.

Verifikation

  • go test ./... grün; gofmt, go vet, golangci-lint sauber
  • Goldens ohne -update grün → kein Verhaltensdrift
  • glabs show Output vor/nach identisch (13/13)
  • CLI Ende-zu-Ende: Happy Path exit=0, Fehlerfall exit=1 mit sauberer Meldung statt Panic/FATAL-Zeile
$ glabs show mpd defaults
Error: course mpd: assignment defaults is abstract (a base for 'extends') and cannot be used directly

🤖 Generated with Claude Code

config/ called log.Fatal (= os.Exit(1)) on every failure, so it could only
ever be used from a CLI: any library or server consumer would be killed
mid-request by a config typo. Turn the 17 exit sites into errors and thread
them out through GetAssignmentConfig/GetCourseConfig; cmd/ is now the only
layer that terminates the process.

The goldens from the previous commit pass unchanged, which is the point: this
is a signature change, not a behaviour change. Cross-checked independently by
diffing `glabs show` output for all 13 assignments in mpd/vss against a binary
built before the refactor — identical. (That comparison needs sorted output:
config.Show() renders deferredBranches straight from a Go map, so its ordering
already varies between runs today. Cosmetic, pre-existing, left alone here.)

Error paths are now testable at all, and config/errors_test.go covers all 15
of them against a new testdata/errors/edge.yaml. It lives outside
testdata/courses/ so the golden test does not try to resolve fixtures that are
meant to fail.

Two bare `log.Fatal()` calls in seeder.go turn out to have been worse than
they looked. zerolog only exits in Msg()/Send(), so a Fatal() with no message
neither logs nor exits: execution fell through to entities[0] on a nil slice
and panicked with index out of range. Both are now errors, with the missing
len(entities) and PrivateKey == nil guards added.

The viper.Set() write-back in the inheritance read path stays for now. Removing
it means threading access to the merged map through ~63 readers — exactly the
work the typed source schema does next, which would then delete that
intermediate again. It disappears for free there, when Resolve() becomes pure
by construction. Documented in place until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@obcode
obcode merged commit 2e74799 into main Jul 17, 2026
9 checks passed
@obcode
obcode deleted the refactor/config-errors branch July 17, 2026 12:59
@github-actions

Copy link
Copy Markdown
Contributor

Coverage

github.com/obcode/glabs/v2/cmd/addgroupguests.go:12:	init						100.0%
github.com/obcode/glabs/v2/cmd/archive.go:12:		init						100.0%
github.com/obcode/glabs/v2/cmd/check.go:10:		init						100.0%
github.com/obcode/glabs/v2/cmd/clone.go:48:		init						100.0%
github.com/obcode/glabs/v2/cmd/delete.go:12:		init						100.0%
github.com/obcode/glabs/v2/cmd/generate.go:12:		init						100.0%
github.com/obcode/glabs/v2/cmd/protect.go:12:		init						100.0%
github.com/obcode/glabs/v2/cmd/push.go:38:		init						100.0%
github.com/obcode/glabs/v2/cmd/report.go:14:		init						100.0%
github.com/obcode/glabs/v2/cmd/root.go:39:		Execute						0.0%
github.com/obcode/glabs/v2/cmd/root.go:43:		init						100.0%
github.com/obcode/glabs/v2/cmd/root.go:51:		er						0.0%
github.com/obcode/glabs/v2/cmd/root.go:56:		initConfig					0.0%
github.com/obcode/glabs/v2/cmd/setaccess.go:12:		init						100.0%
github.com/obcode/glabs/v2/cmd/show.go:8:		init						100.0%
github.com/obcode/glabs/v2/cmd/update.go:12:		init						100.0%
github.com/obcode/glabs/v2/cmd/urls.go:34:		init						100.0%
github.com/obcode/glabs/v2/cmd/version.go:10:		init						100.0%
github.com/obcode/glabs/v2/config/assignment.go:11:	GetCourseURL					0.0%
github.com/obcode/glabs/v2/config/assignment.go:15:	GetAssignmentConfig				98.1%
github.com/obcode/glabs/v2/config/assignment.go:138:	RepoSuffix					100.0%
github.com/obcode/glabs/v2/config/assignment.go:157:	RepoBaseName					100.0%
github.com/obcode/glabs/v2/config/assignment.go:172:	RepoNameWithSuffix				100.0%
github.com/obcode/glabs/v2/config/assignment.go:176:	RepoNameForStudent				100.0%
github.com/obcode/glabs/v2/config/assignment.go:180:	RepoNameForGroup				100.0%
github.com/obcode/glabs/v2/config/assignment.go:184:	coursePath					100.0%
github.com/obcode/glabs/v2/config/assignment.go:195:	GetCourseSubgroupPath				0.0%
github.com/obcode/glabs/v2/config/assignment.go:199:	assignmentPath					100.0%
github.com/obcode/glabs/v2/config/assignment.go:210:	per						100.0%
github.com/obcode/glabs/v2/config/assignment.go:217:	description					100.0%
github.com/obcode/glabs/v2/config/assignment.go:227:	mergeRequest					94.4%
github.com/obcode/glabs/v2/config/assignment.go:271:	mergeRequestApprovals				95.9%
github.com/obcode/glabs/v2/config/assignment.go:351:	extractMergeRequestApprovalRulesRaw		100.0%
github.com/obcode/glabs/v2/config/assignment.go:364:	containsLegacyApprovalUsersKey			68.8%
github.com/obcode/glabs/v2/config/assignment.go:395:	mergeRequestApprovalSettings			95.8%
github.com/obcode/glabs/v2/config/assignment.go:442:	normalizeMergeRequestApprovalConfigKeys		100.0%
github.com/obcode/glabs/v2/config/course.go:15:		GetCourseConfig					100.0%
github.com/obcode/glabs/v2/config/course.go:28:		CourseExists					0.0%
github.com/obcode/glabs/v2/config/course.go:33:		StudentKey					0.0%
github.com/obcode/glabs/v2/config/inheritance.go:27:	assignmentIsAbstract				100.0%
github.com/obcode/glabs/v2/config/inheritance.go:48:	resolveAssignmentInheritance			100.0%
github.com/obcode/glabs/v2/config/inheritance.go:67:	mergedAssignmentMap				100.0%
github.com/obcode/glabs/v2/config/inheritance.go:104:	deepMerge					100.0%
github.com/obcode/glabs/v2/config/inheritance.go:125:	asStringMap					42.9%
github.com/obcode/glabs/v2/config/release.go:8:		release						100.0%
github.com/obcode/glabs/v2/config/release.go:22:	releaseMergeRequest				100.0%
github.com/obcode/glabs/v2/config/release.go:46:	dockerImages					100.0%
github.com/obcode/glabs/v2/config/repo.go:12:		startercode					100.0%
github.com/obcode/glabs/v2/config/repo.go:60:		branches					91.5%
github.com/obcode/glabs/v2/config/repo.go:141:		normalizeBranchRuleConfigKeys			100.0%
github.com/obcode/glabs/v2/config/repo.go:172:		defaultBranch					62.5%
github.com/obcode/glabs/v2/config/repo.go:187:		issues						100.0%
github.com/obcode/glabs/v2/config/repo.go:209:		clone						100.0%
github.com/obcode/glabs/v2/config/repo.go:231:		SetBranch					100.0%
github.com/obcode/glabs/v2/config/repo.go:235:		SetProtectToBranch				88.9%
github.com/obcode/glabs/v2/config/repo.go:253:		SetLocalpath					100.0%
github.com/obcode/glabs/v2/config/repo.go:257:		SetForce					100.0%
github.com/obcode/glabs/v2/config/seeder.go:15:		seeder						63.0%
github.com/obcode/glabs/v2/config/show.go:10:		Show						93.1%
github.com/obcode/glabs/v2/config/slug.go:44:		gitlabProjectPath				100.0%
github.com/obcode/glabs/v2/config/slug.go:60:		gitlabGroupPath					100.0%
github.com/obcode/glabs/v2/config/slug.go:75:		gitlabGroupPathSegment				100.0%
github.com/obcode/glabs/v2/config/slug.go:93:		parameterize					100.0%
github.com/obcode/glabs/v2/config/students.go:14:	SetAccessLevel					100.0%
github.com/obcode/glabs/v2/config/students.go:28:	accessLevel					100.0%
github.com/obcode/glabs/v2/config/students.go:43:	students					100.0%
github.com/obcode/glabs/v2/config/students.go:75:	mkStudents					94.4%
github.com/obcode/glabs/v2/config/students.go:108:	groups						100.0%
github.com/obcode/glabs/v2/config/types.go:5:		String						100.0%
github.com/obcode/glabs/v2/config/urls.go:9:		StartercodeURL					60.0%
github.com/obcode/glabs/v2/config/urls.go:18:		gitURLToWebURL					56.0%
github.com/obcode/glabs/v2/config/urls.go:61:		Urls						100.0%
github.com/obcode/glabs/v2/git/auth.go:11:		GetAuth						90.0%
github.com/obcode/glabs/v2/git/clone.go:18:		Clone						80.0%
github.com/obcode/glabs/v2/git/clone.go:38:		ProjectRepoUrl					100.0%
github.com/obcode/glabs/v2/git/clone.go:44:		localpath					100.0%
github.com/obcode/glabs/v2/git/clone.go:48:		clone						36.1%
github.com/obcode/glabs/v2/git/push.go:17:		Push						0.0%
github.com/obcode/glabs/v2/git/sourcerepo.go:19:	PrepareSourceRepo				0.0%
github.com/obcode/glabs/v2/gitlab/approvals.go:15:	applyMergeRequestApprovalRules			100.0%
github.com/obcode/glabs/v2/gitlab/approvals.go:19:	applyMergeRequestApprovalRulesForMemberCount	80.7%
github.com/obcode/glabs/v2/gitlab/approvals.go:222:	applyMergeRequestApprovalSettings		73.8%
github.com/obcode/glabs/v2/gitlab/approvals.go:285:	approvalRuleName				60.0%
github.com/obcode/glabs/v2/gitlab/approvals.go:295:	approvalRuleAppliesForMemberCount		100.0%
github.com/obcode/glabs/v2/gitlab/approvals.go:305:	resolveApprovalUsernames			77.8%
github.com/obcode/glabs/v2/gitlab/approvals.go:337:	resolveApprovalGroupIDs				75.0%
github.com/obcode/glabs/v2/gitlab/approvals.go:364:	isNumericIdentifier				75.0%
github.com/obcode/glabs/v2/gitlab/archive.go:14:	Archive						77.8%
github.com/obcode/glabs/v2/gitlab/archive.go:32:	archivePerStudent				90.9%
github.com/obcode/glabs/v2/gitlab/archive.go:54:	archivePerGroup					90.9%
github.com/obcode/glabs/v2/gitlab/archive.go:76:	archive						75.6%
github.com/obcode/glabs/v2/gitlab/branches.go:11:	syncConfiguredBranches				50.0%
github.com/obcode/glabs/v2/gitlab/branches.go:47:	defaultBranchName				37.5%
github.com/obcode/glabs/v2/gitlab/branches.go:65:	isBranchAlreadyExistsError			0.0%
github.com/obcode/glabs/v2/gitlab/check.go:9:		CheckCourse					90.6%
github.com/obcode/glabs/v2/gitlab/check.go:67:		checkStudent					100.0%
github.com/obcode/glabs/v2/gitlab/check.go:89:		checkDupsInGroups				100.0%
github.com/obcode/glabs/v2/gitlab/delete.go:11:		Delete						77.8%
github.com/obcode/glabs/v2/gitlab/delete.go:29:		deletePerStudent				100.0%
github.com/obcode/glabs/v2/gitlab/delete.go:40:		deletePerGroup					100.0%
github.com/obcode/glabs/v2/gitlab/delete.go:51:		delete						92.9%
github.com/obcode/glabs/v2/gitlab/generate.go:14:	Generate					38.9%
github.com/obcode/glabs/v2/gitlab/generate.go:56:	generate					0.0%
github.com/obcode/glabs/v2/gitlab/generate.go:273:	generatePerStudent				0.0%
github.com/obcode/glabs/v2/gitlab/generate.go:286:	generatePerGroup				0.0%
github.com/obcode/glabs/v2/gitlab/gitlab.go:13:		NewClient					80.0%
github.com/obcode/glabs/v2/gitlab/groups.go:13:		getGroupIDByFullPath				100.0%
github.com/obcode/glabs/v2/gitlab/groups.go:34:		getGroupID					100.0%
github.com/obcode/glabs/v2/gitlab/groups.go:47:		createGroup					83.3%
github.com/obcode/glabs/v2/gitlab/groups.go:88:		AddGroupGuests					0.0%
github.com/obcode/glabs/v2/gitlab/groups.go:145:	inviteGroupByEmail				0.0%
github.com/obcode/glabs/v2/gitlab/groups.go:163:	addGroupMember					0.0%
github.com/obcode/glabs/v2/gitlab/groups.go:200:	collectUniqueStudents				0.0%
github.com/obcode/glabs/v2/gitlab/issues.go:99:		getStartercodeProject				100.0%
github.com/obcode/glabs/v2/gitlab/issues.go:143:	replicateIssue					37.0%
github.com/obcode/glabs/v2/gitlab/issues.go:202:	resolveIssuePlanForReplication			83.3%
github.com/obcode/glabs/v2/gitlab/issues.go:245:	resolveIssueNumbersForReplication		75.0%
github.com/obcode/glabs/v2/gitlab/issues.go:254:	loadIssueForReplication				69.6%
github.com/obcode/glabs/v2/gitlab/issues.go:298:	listChildIIDsByParentLookup			46.2%
github.com/obcode/glabs/v2/gitlab/issues.go:346:	listChildIIDsByIssueGraphQL			82.4%
github.com/obcode/glabs/v2/gitlab/issues.go:381:	workItemTypeIDForName				0.0%
github.com/obcode/glabs/v2/gitlab/issues.go:406:	attachChildTaskToParent				0.0%
github.com/obcode/glabs/v2/gitlab/issues.go:429:	getProjectPathForGraphQL			25.0%
github.com/obcode/glabs/v2/gitlab/projects.go:12:	generateProject					77.5%
github.com/obcode/glabs/v2/gitlab/projects.go:102:	getProjectByName				80.0%
github.com/obcode/glabs/v2/gitlab/protect.go:15:	ProtectToBranch					77.8%
github.com/obcode/glabs/v2/gitlab/protect.go:33:	protectBranch					100.0%
github.com/obcode/glabs/v2/gitlab/protect.go:37:	protectBranchForMemberCount			71.8%
github.com/obcode/glabs/v2/gitlab/protect.go:119:	hasProtectedBranches				100.0%
github.com/obcode/glabs/v2/gitlab/protect.go:129:	hasMergeRequestApprovalConfig			100.0%
github.com/obcode/glabs/v2/gitlab/protect.go:137:	protectSingleBranch				72.2%
github.com/obcode/glabs/v2/gitlab/protect.go:191:	recreateProtectedBranch				80.0%
github.com/obcode/glabs/v2/gitlab/protect.go:229:	replaceBranchPermissions			88.9%
github.com/obcode/glabs/v2/gitlab/protect.go:245:	isProtectedBranchNotFoundError			75.0%
github.com/obcode/glabs/v2/gitlab/protect.go:254:	protectToBranchPerStudent			90.9%
github.com/obcode/glabs/v2/gitlab/protect.go:276:	protectToBranchPerGroup				72.7%
github.com/obcode/glabs/v2/gitlab/push.go:10:		Push						0.0%
github.com/obcode/glabs/v2/gitlab/report.go:14:		Report						78.9%
github.com/obcode/glabs/v2/gitlab/report.go:51:		ReportHTML					73.7%
github.com/obcode/glabs/v2/gitlab/report.go:86:		ReportJSON					77.8%
github.com/obcode/glabs/v2/gitlab/report_helper.go:17:	report						82.5%
github.com/obcode/glabs/v2/gitlab/report_helper.go:135:	projectReport					85.9%
github.com/obcode/glabs/v2/gitlab/seeder.go:21:		localpath					0.0%
github.com/obcode/glabs/v2/gitlab/seeder.go:25:		runSeeder					0.0%
github.com/obcode/glabs/v2/gitlab/seeder.go:100:	addAndCommit					0.0%
github.com/obcode/glabs/v2/gitlab/seeder.go:121:	push						0.0%
github.com/obcode/glabs/v2/gitlab/setaccess.go:14:	Setaccess					77.8%
github.com/obcode/glabs/v2/gitlab/setaccess.go:32:	setaccess					41.5%
github.com/obcode/glabs/v2/gitlab/setaccess.go:140:	inviteByEmail					100.0%
github.com/obcode/glabs/v2/gitlab/setaccess.go:155:	setaccessPerStudent				100.0%
github.com/obcode/glabs/v2/gitlab/setaccess.go:175:	setaccessPerGroup				80.0%
github.com/obcode/glabs/v2/gitlab/starterrepo.go:16:	pushStartercode					0.0%
github.com/obcode/glabs/v2/gitlab/update.go:15:		Update						60.0%
github.com/obcode/glabs/v2/gitlab/update.go:49:		update						31.6%
github.com/obcode/glabs/v2/gitlab/update.go:96:		updatePerStudent				100.0%
github.com/obcode/glabs/v2/gitlab/update.go:116:	updatePerGroup					100.0%
github.com/obcode/glabs/v2/gitlab/users.go:12:		getUser						95.0%
github.com/obcode/glabs/v2/gitlab/users.go:49:		getUserID					100.0%
github.com/obcode/glabs/v2/gitlab/users.go:63:		addMember					94.1%
github.com/obcode/glabs/v2/main.go:19:			main						0.0%
total:							(statements)					65.2%

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.

1 participant