Repository navigation
Refresh for v1.1.0: Go 1.26, token from gh, semver listing, install latest and uninstall - #5
Merged
Merged
Conversation
go.mod declares Go 1.26.0 as the language version and go1.26.10 as the toolchain. The workflows read the version from go.mod, and the golangci-lint config no longer names one.
github.New takes options, starting with WithBaseURL, so a test can point the client at a fake server. New now returns an error for an invalid base URL.
…1.12.1 go-github v93 builds its client from options and returns release tags as plain strings.
The client passes the token through go-github's WithAuthToken, so golang.org/x/oauth2 is no longer a dependency.
The version came from the build info's dependencies, where the main module never appears, so binaries built with go install reported develop. It now comes from the main module. A (devel) build still reports develop, and a version set through ldflags still wins.
Each action is referenced by the commit its latest release tag points at, with the tag in a trailing comment: checkout v7.0.1, setup-go v7.0.0, upload-artifact v7.0.2, golangci-lint-action v9.3.0 and goreleaser-action v7.3.0.
The release workflow's manual trigger gains a skip-signing input, off by default, that runs GoReleaser with --skip=notarize, so a release can ship unsigned when the Apple membership or certificate is unavailable. Releases from tag pushes always sign.
The assets were named educatesenv-<os>-<arch>_<version>_<os>_<arch>, so the download command in the release notes header returned 404. The binary archive now names them educatesenv-<os>-<arch>, and the build names the binary educatesenv so the two names do not collide.
NewRootCommand builds a fresh tree each time it is called, with flag values captured per tree and the config, GitHub client and version manager loaded by the root's PersistentPreRunE instead of package-level globals, init() registration and cobra.OnInitialize. WithGitHubBaseURL points the GitHub client at another API base URL; it defaults to github.com and is not exposed to users. main builds the tree and executes it.
internal/fakegithub serves paginated release lists, releases by tag and asset downloads from releases each test sets up, records the requests it receives, and lets a test register handlers that take precedence over the defaults. internal/clitest builds a fresh command tree per run with a temporary home and working directory, a PATH that holds only an optional fake gh script, the EDUCATES_* and gh variables removed, and the GitHub client pointed at the fake server, and captures stdout, stderr and the returned error.
list-remote, list, list-local, init, config init, config view and version now fail with cobra's unknown command error when given an argument, before reading the config or calling GitHub. version used ExactArgs(0) and now reports the same error as the others.
Load used viper's package-level instance, so config search paths piled up across loads and values from an earlier load survived when the config file was later missing. Each Load now starts from a new instance.
init, list, list-remote, use, the development-mode warning and the version manager's install progress printed to the process's stdout. They now write to the command's OutOrStdout, so the same text still reaches stdout and a caller that sets its own writers receives it. version.New takes the writer the manager reports to. The version manager tests move to the CLI harness: development-mode cleanup, use, and installing a named version from the fake GitHub server, which replaces the TODO about mocking the GitHub client.
… writer New and Load each printed the warning to the process's stdout, so a command without HOME printed it twice. Load now writes it once to the writer it is given, which the root command sets to cobra's stdout.
TestLoadWithEnvVars read the developer's own ~/.educatesenv/config.yaml, since the config is also searched for in the home directory. Each test now sets HOME to a temporary directory.
errcheck in golangci-lint v2.15.0 reports unchecked fmt.Fprintf and fmt.Fprintln calls on an io.Writer. A failed write to the command's own output leaves nothing to report to, so each call discards the result with _, _ =. The fake server test takes the base URL's address instead of github.Ptr, which govet's inline analyzer reports.
…to refresh-2026/integration
… with the active binary list, use, install and init remove a development symlink left behind when development mode is disabled. version, config and list-remote no longer touch the educates symlink.
… the bin directory The check used a string prefix, so a target in a sibling directory such as bin2 counted as inside the bin directory. It now compares the cleaned directory paths.
…sh time A new pure package holds the release type and the selection rules: a release is a pre-release when its tag has a semver pre-release part or GitHub flags it, drafts and tags that are not full semver never take part, and selection orders by semver or publish time, applies a limit and reports the size of the candidate set.
CutNextDownload makes the next download of an asset announce its full length, send half of it and drop the connection, and serves later downloads whole, so that tests can interrupt a download and retry it.
An interrupted download left a partial binary that a retry then reported as already installed. The binary now downloads into a temporary file in the bin directory, is made executable and only then renamed into place, and the temporary file is removed when any step fails.
use said a version was not installed whatever went wrong. It now says so only when the binary is missing, and otherwise passes the cause through, such as a bin directory it cannot write.
Cover the defaults, case-insensitive keys, each environment override, empty environment variables, the boolean forms and the token sources through the commands, and drop the package tests that read the working directory.
…ected A rate limit gives the time it resets, and without a token points at GH_TOKEN or gh auth login. A 401 names the source of the rejected token. The 404 messages for a missing repository or version are kept.
Document only the commands and flags that exist, with one installation section and one usage section: list-remote's selection flags and output, install latest and checksum verification, uninstall, the config file and its environment overrides, and development mode. Document the token sources in the order they are checked, gh support, and the deprecated config-file token. Match the Go prerequisite and the golangci-lint version to go.mod and CI, and list the Makefile's targets. Add a release section on the signing certificate, notarization, drafts and releasing unsigned with skip-signing, and a macOS note on clearing the quarantine attribute after a browser download.
GoReleaser enabled signing only when MACOS_SIGN_P12 was set, and a missing secret expands to an empty value in Actions, so a tag push without the certificate skipped signing and released unsigned binaries. A release now always signs, so it fails when the certificate is empty or missing. Only --skip=notarize, which the manual trigger's skip-signing input passes, releases unsigned. Snapshot builds still sign only when MACOS_SIGN_P12 is set.
config had no run function, so cobra printed its help for any arguments and exited 0. config now takes no arguments and prints its help when run alone, so 'educatesenv config stray' fails like the other commands that take no arguments.
educatesenv 1.0 read config values through viper's GetBool, which read a value such as 'enabled: yes' or EDUCATES_DEVELOPMENT_ENABLED=yes as false. The YAML loader made such a value an error, so every command failed, version included. A boolean is now read as GetBool read it, and one in a form it did not accept prints a warning on stderr that names the key and file, or the variable. YAML that does not parse is still an error.
use latest failed with a hint to run 'educatesenv install latest', after which use latest still failed, since install resolves latest to a version. It now says that use takes an installed version and suggests 'educatesenv install latest --use', which installs the latest stable release and switches to it.
use's usage line showed its argument as optional, [version|develop], though use requires it, and its not-installed message quoted the install command in backticks where every other message uses single quotes.
Paths.Dir and Paths.File sat beside BinDir without saying which directory and file they were.
The bare source, labels and environment become tokenSource, sourceLabels and tokenVariables.
…tant The config-file token warning and the rate-limit error each spelled out "Set GH_TOKEN or run 'gh auth login'".
The bare Options and Order did not say what they choose or order.
The helper only adds the v prefix that golang.org/x/mod/semver needs, which the name version did not say.
list and uninstall each scanned the bin directory, and found the active version in two ways: list compared the symlink's target with the development binary's path as a string, and uninstall compared files. The symlink that 'use develop' writes is relative, so list never marked develop active after it. The version manager's Installed now lists the installed versions and finds the active one, by comparing files, for both commands.
…once The literals "develop" and "educates" and the binary prefix were spelled out across list, use, uninstall and the version manager. The version manager now has a Develop constant, the name of the educates symlink, and one place each that builds the symlink's path and a version's binary path from platform.BinaryPrefix.
StallNextDownload sends half of an asset and then nothing more until the client gives up. TrickleDownloads sends an asset in chunks spaced apart. CutNextDownload shares the part that announces the full length and sends the first half.
Each read of a download's body waits at most 60 seconds for data, and the request is cancelled when it waits longer. The error says the download stalled and for how long. Nothing caps the whole transfer, so a slow download that keeps making progress finishes. cmd.WithDownloadIdleTimeout shortens the wait for tests.
GoReleaser's changelog needs the previous tag, which a shallow checkout of one commit does not have.
GoReleaser signs and then skips notarization, still succeeding, when a notary secret is empty. The release workflow now checks the five macOS secrets first and fails, naming the missing ones, unless a manual run sets skip-signing.
A run that fails while uploading leaves a draft release, and GitHub's lookup by tag never finds drafts, so a rerun created a second draft for the same tag. GoReleaser now deletes a draft with the same name before creating the release.
Signing and notarization were enabled by whether MACOS_SIGN_P12 was set, so a local build could not turn them on or off on demand, and goreleaser build cannot skip them. MACOS_SIGNING now switches them for release and build alike: a release signs unless it is false, and a snapshot signs only when it is true.
The workflow sets MACOS_SIGNING once for the job, false for a manual run with skip-signing and true otherwise. The secrets check and .goreleaser.yml both read it, replacing the --skip=notarize argument.
While the Apple membership or certificate is unavailable, setting the MACOS_SIGNING repository variable to false makes every run, tag pushes included, release without signing or notarizing the macOS binaries. Each such run adds a warning to its summary.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refreshes educatesenv after 18 months, for v1.1.0: current toolchain and
dependencies, a GitHub token that is never stored or printed, release
listing by semver,
install latest,uninstall, safer downloads, and arelease pipeline that cannot ship unsigned macOS binaries by accident.
What users see:
Where it lives:
Build and release:
workflow from go.mod. go-github v93, cobra 1.10.2, testify 1.12.1;
viper and oauth2 removed.
Dependabot. golangci-lint pinned to v2.15.0 and run on pull requests,
GoReleaser to v2.19.0.
educatesenv-<os>-<arch>, so the downloadcommand in the release notes header works.
MACOS_SIGNINGswitches macOS signing and notarization. The releaseworkflow sets it to
falsefor a manual run withskip-signing, or forevery run while the
MACOS_SIGNINGrepository variable isfalse, andannotates such runs. Otherwise the first step fails the run when any
of the five
MACOS_*secrets is empty, so GoReleaser can no longer signwithout notarizing, or skip signing, and still succeed.
rerun replaces a draft left by a failed run.
Evidence
Before: 7 test functions, none of them on the CLI or the GitHub
client. After: 118, with
go test ./...passing and golangci-lintv2.15.0 reporting 0 issues. A betterleaks scan of the new commits finds
no leaks.
Behavior tests written first, against the old code:
Signing switch,
goreleaser buildwith every signing secret empty:MACOS_SIGNINGfalsetruetruefalseNot verified: the workflow expressions only run on GitHub, so the first
release run is their end-to-end test.
Merge Danger
Door: two-way
Reverting the merge restores v1.0.0's behavior. The one-way step is
tagging and publishing v1.1.0, which stays manual.
Blast Radius: CLI users and the release pipeline
list-remote --allnow means every stable release, andlist-remoteprints a table of the 10 newest by default. Scripts parsing the old
output or relying on
--allfor pre-releases change.config.yamlin the working directory is no longer read.initno longerwrites one.
MACOS_*secrets, or theMACOS_SIGNINGrepository variable set tofalse, or it fails beforecreating a release.