Skip to content

refactor(config): resolve from the source schema, drop viper from course loading#79

Merged
obcode merged 1 commit into
mainfrom
refactor/drop-viper-course-loading
Jul 17, 2026
Merged

refactor(config): resolve from the source schema, drop viper from course loading#79
obcode merged 1 commit into
mainfrom
refactor/drop-viper-course-loading

Conversation

@obcode

@obcode obcode commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Schritt 2b-2 — der Tausch. Baut auf #78 auf (das ist bereits gemergt).

Netto −694 Zeilen. Die ~63 viper.GetString("kurs.assignment.feld")-Aufrufe sind weg; in config/ bleiben zwei viper-Reads, beide für gitlab.host — echte Prozess-Konfiguration.

Der Beweis

Die Goldens laufen unverändert durch. Das ist der ganze Punkt: 67 Assignments über 8 Fixtures lösen durch einen komplett anderen Loader identisch auf.

Quergeprüft am echten CLI: glabs show für 23 Assignments über alle fünf echten Kurse, gegen ein Binary vom Stand vor dem Tausch — identisch.

Was das bringt, über Aufgeräumtheit hinaus

  • Auflösung mutiert keinen globalen State mehr. Der alte Pfad schrieb das gemergte extends-Ergebnis in die viper-Registry zurück — aus einem nominellen Lesevorgang. Ergebnisse hingen damit von der Auflösungsreihenfolge ab, und zwei Kurse ließen sich nicht nebenläufig auflösen. config/-Tests laufen jetzt mit -race.
  • Kursnamen sind aus dem globalen Keyspace raus. Sie wurden bisher neben gitlab.host und coursesfilepath eingemerged — ein Kurs namens courses hätte mit der Kursliste kollidiert.
  • Eine fehlende Kursdatei ist ein Fehler, der die Datei nennt, kein panic:
    Error: cannot find a course file for "gibtsnicht": looked for gibtsnicht.yaml
    and gibtsnicht.yml in "/Users/obraun/Documents/HM/lectures/glabs-yaml"
    

Ein Fund über meinen eigenen Test aus #78

Zwei Validierungen fehlten im reinen Resolver — startercode ohne url und der abgelehnte users-Key. Gefangen hat sie der Golden-Lauf, nicht der differentielle Test, der die Äquivalenz doch beweisen sollte.

Grund: dessen Fehlerliste war eine handkopierte Teilmenge von errors_test.go — 10 von 13. Und natürlich waren die zwei ausgelassenen genau die zwei, die falsch waren. Merke: ein Test, der Fälle von Hand aufzählt, deckt nur die Fälle ab, an die jemand gedacht hat. Die Goldens haben es gerettet, weil sie nicht aufzählen.

Eine Verhaltensänderung, bewusst

RepoSuffix bedeutet für handgebaute Configs jetzt etwas anderes. Es zog früher viper zu Rate, um zu entscheiden, ob useEmailDomainAsSuffix überhaupt gesetzt war — ein nullwertiges Struct hieß damit „nimm die Domain", obwohl das Feld false sagte. Diese Krücke kann nicht überleben, wenn die Kursdateien viper verlassen.

Da die Auflösung das Feld immer setzt (abwesend heißt true), ändert sich für aufgelöste Configs nichts. Und AssignmentConfig wird nirgends von Hand gebaut außer im Resolver selbst (geprüft). Das Feld sagt jetzt endlich, was es heißt.

Die Tests

Die alten trieben config/ über 137 viper.Set-Aufrufe auf der globalen Registry. Sie schreiben jetzt das YAML, das sie testen — näher an dem, was ein Nutzer wirklich hat, und als Konfiguration lesbar statt als Zuweisungshaufen:

registerCourse(t, `
course:
  a1:
    startercode:
      url: git@example.org:starter.git
`)
s := mustAssignmentConfig(t, "course", "a1").Startercode

Abdeckung unverändert. Der differentielle Test aus #78 fällt weg — es gibt keine zweite Implementierung mehr zum Vergleichen.

Verifikation

  • go test ./... grün, -race grün; gofmt, go vet, golangci-lint sauber
  • Goldens unverändert (67 Assignments) → Tausch bewiesen
  • glabs show 23/23 identisch gegen die echten Kursdateien
  • Fehlerpfade am CLI geprüft; config lint weiterhin exit=0, config fmt unverändert

🤖 Generated with Claude Code

…rse loading

Switches resolution to the pure resolver added in the previous commit and
deletes the viper-based loader. config/ no longer reads course files through
viper at all: the ~63 viper.GetString("course.assignment.field") calls are gone,
and the only two viper reads left are gitlab.host, which is genuinely global
process configuration.

What this buys, beyond tidiness:

  - Resolution no longer mutates global state. The old path wrote the merged
    `extends` result back into the viper registry from what was nominally a read,
    so results depended on resolution order and two courses could not be resolved
    concurrently. config/ tests now run with -race.
  - Course names are out of the global keyspace. They used to be merged in
    alongside gitlab.host and coursesfilepath, so a course named `courses` would
    have collided with the course list.
  - A missing course file is an error naming the file, not a panic.

The goldens pass unchanged, which is the whole point: 67 assignments across 8
fixtures resolve identically through a completely different loader. Cross-checked
end to end by diffing `glabs show` for 23 assignments across all five real
courses against a binary built before the switch — identical.

Two validations turned out to be missing from the pure resolver, both caught by
the golden run rather than by the differential test that was supposed to prove
equivalence: startercode without a url, and the rejected `users` key. The
differential test's error list was a hand-copied subset of errors_test.go's — 10
of 13 — and of course the two it omitted were the two that were wrong. Worth
remembering: a test that enumerates cases by hand only covers the cases someone
remembered.

RepoSuffix changes meaning for hand-built configs. It used to consult viper to
decide whether UseEmailDomainAsSuffix had been set at all, so a zero-valued
struct meant "use the domain" even though the field said false. That crutch
cannot survive course files leaving viper, and resolution always sets the field
(absent means true), so the field now simply means what it says. Nothing but the
resolver builds an AssignmentConfig.

The old tests drove config by setting 137 keys on the global viper registry.
They now write the YAML they are testing, which is both closer to what a user has
and readable as configuration rather than as a pile of assignments. Their
coverage is unchanged; the differential test goes, having no second
implementation left to compare against.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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/config.go:13:		init						100.0%
github.com/obcode/glabs/v2/cmd/config.go:119:		rewriteCourseFile				0.0%
github.com/obcode/glabs/v2/cmd/config.go:150:		decodeCourseFile				0.0%
github.com/obcode/glabs/v2/cmd/config.go:164:		coursesToProcess				0.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:41:		Execute						0.0%
github.com/obcode/glabs/v2/cmd/root.go:45:		init						100.0%
github.com/obcode/glabs/v2/cmd/root.go:53:		er						0.0%
github.com/obcode/glabs/v2/cmd/root.go:58:		initConfig					0.0%
github.com/obcode/glabs/v2/cmd/root.go:95:		findCourseFile					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:17:	RepoSuffix					100.0%
github.com/obcode/glabs/v2/config/assignment.go:36:	RepoBaseName					100.0%
github.com/obcode/glabs/v2/config/assignment.go:51:	RepoNameWithSuffix				100.0%
github.com/obcode/glabs/v2/config/assignment.go:55:	RepoNameForStudent				100.0%
github.com/obcode/glabs/v2/config/assignment.go:59:	RepoNameForGroup				100.0%
github.com/obcode/glabs/v2/config/course.go:12:		StudentKey					0.0%
github.com/obcode/glabs/v2/config/courses.go:33:	LoadCourseFile					68.8%
github.com/obcode/glabs/v2/config/courses.go:61:	ResetCourses					100.0%
github.com/obcode/glabs/v2/config/courses.go:69:	courseBody					100.0%
github.com/obcode/glabs/v2/config/courses.go:85:	CourseExists					100.0%
github.com/obcode/glabs/v2/config/courses.go:90:	globals						100.0%
github.com/obcode/glabs/v2/config/courses.go:95:	GetAssignmentConfig				100.0%
github.com/obcode/glabs/v2/config/courses.go:104:	GetCourseConfig					100.0%
github.com/obcode/glabs/v2/config/courses.go:114:	GetCourseSubgroupPath				71.4%
github.com/obcode/glabs/v2/config/courses.go:127:	GetCourseURL					0.0%
github.com/obcode/glabs/v2/config/decode.go:72:		DecodeCourse					100.0%
github.com/obcode/glabs/v2/config/decode.go:103:	DecodeCourseBody				87.5%
github.com/obcode/glabs/v2/config/decode.go:151:	decodeInto					75.0%
github.com/obcode/glabs/v2/config/decode.go:167:	prefixPaths					90.9%
github.com/obcode/glabs/v2/config/decode.go:188:	approvalsDecodeHook				100.0%
github.com/obcode/glabs/v2/config/decode.go:203:	normalizeLegacyKeys				100.0%
github.com/obcode/glabs/v2/config/encode.go:25:		EncodeCourse					66.7%
github.com/obcode/glabs/v2/config/inheritance.go:21:	deepMerge					100.0%
github.com/obcode/glabs/v2/config/inheritance.go:42:	asStringMap					42.9%
github.com/obcode/glabs/v2/config/lint.go:29:		String						0.0%
github.com/obcode/glabs/v2/config/lint.go:39:		Lint						100.0%
github.com/obcode/glabs/v2/config/lint.go:68:		unknownKeyHint					75.0%
github.com/obcode/glabs/v2/config/lint.go:79:		lintAssignment					87.2%
github.com/obcode/glabs/v2/config/lint.go:177:		validWhenCommitAdded				66.7%
github.com/obcode/glabs/v2/config/lint.go:186:		sortedAssignmentNames				100.0%
github.com/obcode/glabs/v2/config/repo.go:3:		defaultBranch					62.5%
github.com/obcode/glabs/v2/config/repo.go:18:		SetBranch					100.0%
github.com/obcode/glabs/v2/config/repo.go:22:		SetProtectToBranch				88.9%
github.com/obcode/glabs/v2/config/repo.go:40:		SetLocalpath					100.0%
github.com/obcode/glabs/v2/config/repo.go:44:		SetForce					100.0%
github.com/obcode/glabs/v2/config/resolve.go:34:	ResolveAssignment				87.0%
github.com/obcode/glabs/v2/config/resolve.go:84:	lookupAssignment				87.5%
github.com/obcode/glabs/v2/config/resolve.go:98:	reservedCourseKey				100.0%
github.com/obcode/glabs/v2/config/resolve.go:110:	containsApprovalUsersKey			100.0%
github.com/obcode/glabs/v2/config/resolve.go:133:	truthy						75.0%
github.com/obcode/glabs/v2/config/resolve.go:146:	mergeAssignment					94.7%
github.com/obcode/glabs/v2/config/resolve.go:183:	buildAssignmentConfig				100.0%
github.com/obcode/glabs/v2/config/resolve.go:238:	resolvePer					100.0%
github.com/obcode/glabs/v2/config/resolve.go:245:	resolveDescription				100.0%
github.com/obcode/glabs/v2/config/resolve.go:252:	resolveAccessLevel				100.0%
github.com/obcode/glabs/v2/config/resolve.go:264:	resolveAssignmentPath				100.0%
github.com/obcode/glabs/v2/config/resolve.go:277:	ResolveCourse					80.0%
github.com/obcode/glabs/v2/config/resolve.go:291:	ResolveCoursePath				85.7%
github.com/obcode/glabs/v2/config/resolve.go:303:	decodeCourseSettings				71.4%
github.com/obcode/glabs/v2/config/resolve.go:315:	resolveStartercode				100.0%
github.com/obcode/glabs/v2/config/resolve.go:347:	resolveBranches					95.0%
github.com/obcode/glabs/v2/config/resolve.go:410:	resolveIssues					100.0%
github.com/obcode/glabs/v2/config/resolve.go:435:	resolveClone					100.0%
github.com/obcode/glabs/v2/config/resolve.go:450:	resolveRelease					86.7%
github.com/obcode/glabs/v2/config/resolve.go:480:	resolveDeferredBranches				100.0%
github.com/obcode/glabs/v2/config/resolve.go:522:	resolveMergeRequest				100.0%
github.com/obcode/glabs/v2/config/resolve.go:567:	resolveApprovalRules				100.0%
github.com/obcode/glabs/v2/config/resolve.go:599:	trimUnique					100.0%
github.com/obcode/glabs/v2/config/resolve.go:616:	trimNonEmpty					100.0%
github.com/obcode/glabs/v2/config/resolve.go:626:	resolveApprovalSettings				91.7%
github.com/obcode/glabs/v2/config/resolve.go:662:	resolveStudents					100.0%
github.com/obcode/glabs/v2/config/resolve.go:679:	resolveGroups					100.0%
github.com/obcode/glabs/v2/config/resolve.go:722:	filterByPatterns				100.0%
github.com/obcode/glabs/v2/config/resolve.go:737:	resolveSeeder					100.0%
github.com/obcode/glabs/v2/config/show.go:10:		Show						93.1%
github.com/obcode/glabs/v2/config/signkey.go:26:	parseSignKey					33.3%
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:12:	SetAccessLevel					100.0%
github.com/obcode/glabs/v2/config/students.go:30:	matchesPattern					100.0%
github.com/obcode/glabs/v2/config/students.go:35:	mkStudents					94.4%
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					56.6%
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.6%

@obcode
obcode merged commit af9d1e1 into main Jul 17, 2026
10 checks passed
@obcode
obcode deleted the refactor/drop-viper-course-loading branch July 17, 2026 15:01
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