goenv: Trois Fonctions, Une Variable d’Env, Rien à Foutre de Plus

Chaque service que j’écris a besoin d’exactement une information avant de faire quoi que ce soit d’autre: est-ce que ce truc tourne en prod, ou est-ce qu’un pauvre con (moi, à 2h du mat, une demi-bouteille dans le nez) est encore en train de le tripoter en local. C’est tout le besoin. Pendant des années, la réponse a vécu sous la forme d’un os.Getenv("ENV") et d’un if de deux lignes, copié-collé dans chaque main.go que j’ai jamais écrit, orthographié un peu différemment à chaque fois. Alors j’ai fait ce que fait tout ingénieur raisonnable face à un problème de deux lignes: j’ai écrit un paquet entier pour ça, je lui ai mis un README avec une section FAQ, j’ai taggé onze releases et je l’ai livré avec une couverture de tests complète. Voilà goenv.

Le Problème Qui N’en Est Pas Vraiment Un

Il n’y en a pas, et c’est toute la blague. os.Getenv("ENV") fait seize caractères. Le comparer à "dev" en fait onze de plus. Tu pourrais écrire le test entier inline en moins de temps qu’il ne t’en a fallu pour lire cette phrase. Sauf que tu l’aurais inline, dans chaque service, orthographié un peu différemment à chaque fois, os.Getenv("ENVIRONMENT") ici, APP_ENV là, quelqu’un qui teste == "development" au lieu de "dev" dans un troisième repo, et voilà que ta logique du “je suis en prod?” devient un champ de mines éparpillé sur tous les services que tu possèdes, chacun câblé de travers à sa façon bien à lui. goenv existe pour que cette question ait exactement une implémentation, définie exactement à un endroit, importée partout où ça compte.

L’Implémentation Entière

Voilà l’implémentation entière. Pas un extrait, le fichier complet:

package goenv
import (
	"os"
)
const EnvVarName = "ENV"
type Type = string
const (
	Prod Type = "prod"
	Dev  Type = "dev"
)
func Get() Type {
	e := os.Getenv(EnvVarName)
	switch e {
	case Dev:
		return Dev
	default:
		return Prod
	}
}
func IsProd() bool {
	return Get() == Prod
}
func IsDev() bool {
	return Get() == Dev
}

Un seul import. Même pas fmt. Un switch avec un seul vrai cas et un default qui atterrit toujours sur Prod. Tu poses ENV=dev et tu récupères Dev. Tu le poses sur n’importe quoi d’autre, prod, staging, test, une chaîne vide, une faute de frappe, ou rien du tout, et tu récupères Prod. goenv ne te fait pas confiance pour avoir écrit les choses correctement, donc il n’essaie même pas de deviner. Si ce n’est pas explicitement dev, c’est prod. Pas de juste milieu, pas de bénéfice du doute.

La surface exportée, c’est exactement trois fonctions: Get() renvoie le Type courant (“prod” ou “dev”), et IsProd() / IsDev() sont les deux commodités booléennes qui se contentent d’appeler Get() et de comparer. Trois constantes les soutiennent, EnvVarName (“ENV”), Prod et Dev, plus un alias Type. Et je veux être honnête sur celui-là, parce qu’il est écrit type Type = string, avec le signe égal, ce qui en fait un vrai alias, pas un type défini. Il est interchangeable avec string dans les deux sens. Tu n’en tires strictement aucune sécurité à la compilation, rien ne t’empêche de passer "banana" là où un Type est attendu. C’est de la documentation avec des étapes en plus, et prétendre le contraire serait exactement le genre de chose dont ce paquet existe pour se moquer. Voilà toute l’API. Pas de struct de config, pas d’options fonctionnelles, pas d’interface à implémenter, pas de WithLogger(). Trois fonctions, et tu les connais déjà toutes.

L’Utiliser (Il N’y a Pas Grand-Chose à Utiliser)

go get github.com/psyb0t/goenv
package main
import (
	"fmt"
	"github.com/psyb0t/goenv"
)
func main() {
	if goenv.IsProd() {
		fmt.Println("don't fuck this up")
	}
	if goenv.IsDev() {
		fmt.Println("break whatever you want")
	}
}

Et côté déploiement, c’est juste une variable d’environnement comme toutes les autres:

export ENV=dev   # you're developing, go break shit
export ENV=prod  # you're in production, don't
export ENV=      # also production, because paranoia is a feature

C’est exactement l’astuce qu’utilise servicepack pour sa propre détection d’environnement, même paquet, même variable ENV, même défaut du “à moins que tu dises explicitement dev, tu es en prod”. Une dépendance, un comportement, réutilisé partout au lieu d’être réinventé service par service.

Ce Qu’il y a Vraiment Dans la Boîte

  • Zéro dépendance, go.mod tient en une ligne de module et une version de Go, rien d’autre. Le seul import de tout goenv.go est os.
  • Une vraie couverture de tests, pas une vantardise de README, j’ai tiré la source et lancé go test -cover ./... moi-même: 100.0% of statements. Dix sous-tests répartis sur trois fonctions de test couvrent chaque branche, dev, prod, chaîne vide, et une valeur non reconnue (“staging”) qui doit elle aussi retomber sur prod, parce que les tests vérifient que la règle du “tout ce qui n’est pas explicitement dev est prod” tient réellement.
  • Défaut sur prod, vérifiable, c’est écrit noir sur blanc dans le switch: le seul chemin qui renvoie Dev est une correspondance exacte sur ENV=dev. Non défini, vide, mal orthographié, “staging”, “test”, peu importe, tout se résout en Prod. Le paquet ne te fait pas confiance et il ne s’en cache pas.
  • Onze releases taggées, v1.0.0 (“fuck yeah prod or dev”), v1.0.1 (“peen”), v1.0.2, qui a ajouté un skill d’agent ClawHub, et huit autres qui n’ont changé aucune ligne de bibliothèque. Oui. Un paquet de trois fonctions qui lit une variable d’environnement livre désormais un skill d’agent documenté, pour qu’on puisse expliquer à une IA comment appeler IsProd(). Le fichier de skill est plus long que la bibliothèque qu’il documente. Ensuite sont venus les badges, les mentions de licence, un miroir Codeberg, une scission du pipeline de CI, Go 1.26, et une release dont le changelog complet tient dans le fait que le résumé de surface du README avait oublié de mentionner Type. goenv.go compte exactement un commit dans toute son histoire, le premier, donc chaque release après la première est de l’emballage, de la paperasse ou une correction de coquille, ce qui est à la fois le dénouement le plus drôle et le plus juste disponible. Toutes les onze bien réelles, toutes les onze passées par le même pipeline de CI que tous les autres paquets que je livre: go vet, go test -race, et un seuil de couverture réglé à 90% minimum dans le Makefile, et depuis la v1.0.2 un job de publication déclenché par tag qui envoie le skill sur ClawHub une fois le lint et les tests passés, une barre que ce paquet franchit avec dix points pleins d’avance en ne faisant pratiquement rien.

En Résumé

Si tu as besoin de plus d’un bit d’information sur ton environnement, des types, des valeurs par défaut, des structs, tout le bordel, va plutôt lire ce qui se dit sur gonfiguration, c’est la version adulte de cette idée. goenv est à l’autre bout du spectre: il répond à une question, il y répond pareil à chaque fois, et il ne prétend pas être quoi que ce soit de plus.

Chope-le sur GitHub, ou écris os.Getenv("ENV") == "dev" toi-même comme une personne normale. Les deux marchent. Je ne suis pas ton chef.

L’Installer Dans Ton Agent

Oui, le paquet de trois fonctions a un plugin. Non, je n’en dirai pas plus. Tout ce qui est sous .agents/ est catalogué dans un seul marketplace, donc ça fait deux commandes:

claude plugin marketplace add psyb0t/agents
claude plugin install goenv@psyb0t

Codex utilise le même marketplace avec un verbe différent, codex plugin add goenv@psyb0t, parce qu’il n’existe pas de codex plugin install. Il trouve aussi le skill tout seul dans un checkout du repo, puisqu’il scanne .agents/skills/ nativement sans que rien ne soit installé.