Module Versioning — Tasks¶
Table of Contents¶
How to Use This File¶
Each task is a hands-on exercise. Most can be completed in a scratch directory; some require pushing to a Git host (use a personal account or a local bare repo). All tasks include the expected outcome you should observe, so you can self-grade.
Work through them in order. Easy tasks build the muscle memory for tags; medium tasks introduce pseudo-versions, replace, and pre-releases; hard tasks handle major-version migrations and +incompatible; stretch tasks build tooling.
Easy¶
Task 1 — Tag your first version¶
- Create a new directory and
go mod init example.com/v-task-1. - Add a single file
hello.gowith one exported functionGreet. - Commit to a fresh Git repo.
- Tag the commit
v0.1.0. - Run
git tag --listand verify onlyv0.1.0appears.
Expected outcome: A tagged commit and a clean git log --oneline --decorate showing the tag.
Task 2 — Bump for a feature¶
Continuing from Task 1:
- Add a second exported function
GreetLoud. - Commit.
- Tag the new commit appropriately.
Expected outcome: The new tag is v0.2.0. (Adding a feature is a minor bump, even at v0.)
Task 3 — Bump for a bug fix¶
Continuing from Task 2:
- Fix a bug in
Greet— say, change"Hello "to"Hello, ". - Commit.
- Tag appropriately.
Expected outcome: The new tag is v0.2.1. (Bug fixes are patches.)
Task 4 — Reach v1.0.0¶
Continuing from Task 3:
- Decide that the API is stable.
- Tag a release.
Expected outcome: The tag is v1.0.0. Bonus: write a one-paragraph CHANGELOG entry explaining what reaching v1 means.
Task 5 — Spot the invalid version¶
For each tag below, decide whether Go accepts it as a module version:
| Tag | Valid? |
|---|---|
v1.2.3 | ? |
1.2.3 | ? |
V1.2.3 | ? |
v1.2 | ? |
v1.2.3.4 | ? |
v1.2.3-rc.1 | ? |
v1.2.3-rc.1+meta | ? |
v1.0.0+incompatible | ? |
v01.2.3 | ? |
v1.2.3- | ? |
Expected outcome: valid: 1, 6, 7, 8 (only with the right tag history). Invalid: 2 (no v), 3 (capital), 4 (only two parts), 5 (four parts), 9 (leading zero), 10 (trailing dash).
Task 6 — List versions of a real module¶
Expected outcome: A space-separated list of every tagged version of uuid. Note the absence of pseudo-versions (only canonical tags appear).
Task 7 — Read a require line¶
Open any project's go.mod (or use the snippet below):
Answer:
- Which version of
uuidis pinned? - Why does
cobrahave// indirect? - What would happen if you deleted the
// indirect?
Expected outcome: (1) v1.6.0. (2) Because some other dependency requires cobra, not your code directly. (3) go mod tidy would re-add it.
Medium¶
Task 8 — Trigger a pseudo-version¶
- Create a public Git repo on your own host (or a local bare repo) at
example.com/v-task-8. - Push a commit without tagging.
- From a separate consumer module, run:
- Open
go.modand inspect therequireline.
Expected outcome: The version is a pseudo-version of the form v0.0.0-<timestamp>-<hash>. The timestamp matches the commit timestamp; the hash is the first 12 hex chars of the commit SHA.
Task 9 — Decode a pseudo-version¶
Given the pseudo-version v1.5.1-0.20240612103515-abc123def456:
- What was the latest tag before this commit?
- What is the commit timestamp in human-readable form?
- Where would you look to find the full commit hash?
Expected outcome: (1) v1.5.0 — pseudo-versions use base PATCH+1. (2) 2024-06-12 10:35:15 UTC. (3) Search for the hash prefix abc123def456 in the repo's git log.
Task 10 — Use a replace directive for local development¶
- Create two sibling modules:
example.com/libandexample.com/app. appshould importlib.- Without publishing
lib, makeappbuild by adding areplacedirective inapp'sgo.mod: - Verify
go build ./...from theappdirectory succeeds.
Expected outcome: The build works without lib having any tag or being on a Git host.
Task 11 — Pre-release a v1.0.0¶
- Continue from Task 4 (you have a
v1.0.0). - Add a half-finished feature — say, a function that always returns an error.
- Commit, then tag
v1.1.0-rc.1. - From a consumer module, try
go get yourmod@latestandgo get yourmod@v1.1.0-rc.1.
Expected outcome: @latest resolves to v1.0.0 (skips the rc). @v1.1.0-rc.1 succeeds. This proves pre-releases are opt-in.
Task 12 — Retract a release¶
- In your library, deliberately introduce a bug and tag
v1.0.1. - Realise the mistake.
- Fix the bug, then in
go.modadd: - Tag
v1.0.2. - From a consumer, run
go list -m -u all.
Expected outcome: The output marks v1.0.1 as retracted and notes the retraction reason. @latest skips v1.0.1.
Task 13 — Pin to a specific commit¶
- Find a public Go library on GitHub.
- Pick a commit hash from the repo's
mainbranch (without a tag). - Run
go get example.com/lib@<hash>. - Open
go.mod.
Expected outcome: The require line shows a pseudo-version derived from your commit. Now switch to a tagged version with go get @v<latest> and observe how go.mod changes.
Task 14 — Resolve a transitive conflict¶
- Create a module
example.com/app. - Add a dependency on
example.com/Awhich itself depends onexample.com/X v1.2.0. - Add a dependency on
example.com/Bwhich itself depends onexample.com/X v1.5.0. - Run
go mod tidy. - What version of
Xends up in yourgo.mod? Why?
Expected outcome: X v1.5.0 — MVS picks the highest version anyone in the graph requires.
Task 15 — Use go mod why¶
In any non-trivial Go module:
Expected outcome: A path showing how the indirect dep is reached from your main module. Useful for "why is this in my graph?"
Hard¶
Task 16 — Bump from v1 to v2¶
- Take your library at
v1.x.x. - Decide on a real breaking change (rename a function, restructure a type).
- Update the
moduleline ingo.modto end with/v2. - Update every internal import inside the module to include
/v2. - Tag
v2.0.0and push. - From a consumer module, install both
libandlib/v2. Confirm both can be imported in different files.
Expected outcome: - lib's go.mod says module example.com/yourmod/v2. - Internal imports include /v2. - A consumer can import (a "example.com/yourmod"; b "example.com/yourmod/v2") and the two are independent types.
Task 17 — Subfolder layout for v2¶
- In a fresh repo, create
lib/go.modwithmodule example.com/lib. - Add a
lib/v2/go.modwithmodule example.com/lib/v2. - Both versions live on
main. - Tag
v1.0.0andv2.0.0. - Verify a consumer can install either independently:
Expected outcome: Two independent modules from one repo, with their own tags.
Task 18 — Diagnose +incompatible¶
- Create a repo without SIV:
module example.com/legacy(no/v2). - Tag
v2.0.0. - From a consumer, run
go get example.com/legacy@v2.0.0. - Inspect
go.mod.
Expected outcome: require example.com/legacy v2.0.0+incompatible. The +incompatible marker appears because the module path does not declare the /v2 suffix.
Task 19 — Migration helper¶
- In your
v2library, add acompat/sub-package that re-exports the v1 API but implemented in terms of v2. - Document it: "Use
compatonly during migration; remove imports once the migration is complete."
Expected outcome: A consumer can switch from lib to lib/v2/compat with zero code changes, then migrate file-by-file to native lib/v2 calls.
Task 20 — Multi-module repo¶
- Create a repo with two modules:
- Tag
cli/v1.0.0andlib/v0.1.0. - Verify a consumer can install either independently.
Expected outcome: Two independent modules in one repo, each with prefixed tags. git tag --list shows both prefixes.
Task 21 — Run gorelease¶
- In your library, run:
- Make a deliberately breaking change (rename an exported function).
- Re-run
gorelease.
Expected outcome: The first run reports compatibility with v1.5.0. After the breaking change, gorelease recommends a major bump (v2.0.0) and lists the offending changes.
Task 22 — Replace with a fork¶
- Fork a public library on GitHub.
- Make a small change in the fork (say, add a comment).
- In your application's
go.mod: - Build and run.
Expected outcome: The fork's bytes are used. go mod graph shows the replace edge.
Stretch¶
Task 23 — Build a "next version" suggester¶
Write a CLI that: 1. Takes the current version (e.g., v1.5.0). 2. Reads commit messages since that tag. 3. Categorises each commit as breaking / feature / fix based on a convention (Conventional Commits prefixes: feat:, fix:, BREAKING CHANGE:). 4. Suggests the next version: major if any commit is breaking, else minor if any is feat:, else patch.
Example output:
Task 24 — Verify a go.sum¶
Write a script that reads go.sum and verifies the cache:
- Walks every entry.
- Locates the corresponding zip in
$GOMODCACHE/cache/download/.../@v/<version>.zip. - Computes the
h1:hash. - Compares to the
go.sumentry.
Expected outcome: All hashes match (otherwise your cache is tampered or corrupted).
Task 25 — Plot the dependency graph¶
Use go mod graph and Graphviz to produce a PNG of your project's dependency graph. Annotate edges with the version selected by MVS vs the version originally required.
Task 26 — Audit for replace debt¶
Write a tool that scans every go.mod in a repo and reports replace directives. Categorise:
- Local-path replaces (
=> ../foo). - Fork replaces (
=> github.com/myfork/foo). - Pin replaces (
=> github.com/foo/foo v1.5.0).
Use this to track "replace debt" over time.
Rubric¶
| Score | Meaning |
|---|---|
| Easy: 7/7 | You have semver vocabulary and tag fluency. |
| Medium: 8/8 | You can manage day-to-day version operations confidently. |
| Hard: 7/7 | You can lead a major version migration and handle multi-module repos. |
| Stretch: 4/4 | You can build versioning tooling. |
If you fall behind in any category, return to the corresponding level file (junior, middle, senior, professional) and re-read the relevant sections before continuing.