Skip to content

feat(librarystore): versioned library store + GitHub Go strategy (#446) - #447

Merged
antoinetoussaint-byte merged 4 commits into
mainfrom
feat/library-store
Aug 23, 2026
Merged

feat(librarystore): versioned library store + GitHub Go strategy (#446)#447
antoinetoussaint-byte merged 4 commits into
mainfrom
feat/library-store

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byteantoinetoussaint-byte commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes#446.

First step of the library-publishing epic (P0). Mirrors the #443 durability principle for libraries: publish them as durable, versioned artifacts a native package manager can pull without the codefly toolchain or local source.

Summary

  • pkg/librarystore — a backend-agnostic Store (Publish / Resolve / List) over Coordinates{language,name,version}Published{importPath, immutable ref, digest, install hint}. Keeps PyPI/Artifactory/npm-registry/Go-proxy swappable later.
  • GitHub Go strategy — publishes each Go export to its own repo tagged vX.Y.Z, so go get github.com/<owner>/<name>-go@vX.Y.Z resolves natively (a git repo with semver tags is a Go module source). Published versions are immutable (republish refused); Resolve picks the highest tag satisfying a semver constraint and pins the tag's commit; treeDigest gives a deterministic content digest.
  • Python / TypeScript return an explicit not implemented yet error — deferred to P1 (PEP 503 index / GitHub Packages).

Scope

This is the store foundation only. Follow-up P0 steps (own PRs): the codefly export library command, the library-packaging agent RPC, and the resolver "published mode" that wires go get/pip/npm into consumer manifests (implementing install library).

Test plan

  • go build / go vet / golangci-lint clean
  • End-to-end against a real local bare git repo (no mocks): publish v1.0.0 → publish v1.2.0 → List newest-first → Resolve ^1.0.0 → 1.2.0 (commit-pinned); republish same version rejected; non-semver + unsupported-language rejected; treeDigest deterministic + content-sensitive.

@antoinetoussaint-byte

Copy link
Copy Markdown
ContributorAuthor

CI red herring: all 5 checks failed in ~3s with zero steps executed — GitHub Actions annotation: "The job was not started because your account is locked due to a billing issue." Same failure hit every run in the repo since ~15:35 UTC (including main's own push CI for #449 and PR #451). Branch is verified green locally on this exact commit (build + vet + 16/16 tests + golangci-lint 0 issues under Go 1.27). Once billing is unlocked: gh run rerun 32583304704 — no push needed.

… strategy
First step of the durable library-publishing epic (#446): a backend-agnostic
Store that publishes and resolves library exports as versioned artifacts a
native package manager can consume without the codefly toolchain.
- Store interface (Publish/Resolve/List) over Coordinates{language,name,version}
→ Published{importPath, immutable ref, digest, install hint}.
- GitHubStore Go strategy: publishes each Go export to its own repo tagged
vX.Y.Z, so `go get github.com/<owner>/<name>-go@vX.Y.Z` resolves natively.
Published versions are immutable (republish is refused); Resolve selects the
highest tag satisfying a semver constraint and pins the tag's commit.
- Python/TypeScript return an explicit not-implemented error (P1).
Tested end-to-end against a real local bare git repository (publish → list →
resolve → immutability), no mocks.
Part of #446.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…review fixes
Review of the store found the product promise itself was unverified, and four
ways real inputs break the publish/resolve path. Fix the causes:
- Module-path validation: `go get <importPath>@<tag>` only works when the
published go.mod declares exactly that module path, and versions are
immutable — so a mismatched release could never be corrected. Publish now
requires go.mod to declare the derived import path, failing before any git
operation. The e2e test previously enshrined the broken case (published
example.com/authkit into an authkit-go repo); it now publishes the matching
path, and a dedicated test proves rejection fires pre-clone.
- Source .git collision: replaceTrackedTree's os.CopyFS rewrite dropped the
.git skip, so publishing a library that is itself a git checkout would
interleave its .git into the clone's .git before failing. Restore the skip
(root-scoped walk, symlinks still rejected).
- Identical-content releases: a version bump with no code change staged an
empty diff and `git commit` failed. Commit with --allow-empty.
- Silent empty Ref: Resolve read the tag commit from a second ls-remote whose
peeled `^{}` line does not exist for lightweight tags, yielding Ref:"" with
no error. One ls-remote now feeds both List and Resolve (peeled commit wins,
lightweight hash falls back, no listing/lookup race) and the ref is
validated as a commit hash.
- Digest parity and modes: Resolve returned Digest:"" though Published
documents it; it now computes the digest from a shallow clone at the tag.
treeDigest folds in the git-normalized mode (755/644) so an executable-bit
flip changes the digest — the same lesson the docker-build-recipe v2
contract encoded — while staying identical between the publish-side source
and a resolve-side clone; .git is excluded on both sides.
- Publish honors the repository's default branch (symbolic-ref) instead of
forcing main; a missing remote repo now errors with create-it-first
guidance; Published.Version is canonicalized (no leading v) on both paths;
prerelease exclusion under plain constraints is pinned by test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up review of the store surfaced three remaining defects:
- Digest domain mismatch: Publish hashed the raw source dir while Resolve
hashed a clone, so a .gitignore'd file made the two disagree — breaking the
Published "the two agree" contract. treeDigest now hashes the git-tracked
file set (`git ls-files`) on both paths, so it describes exactly the
published content and Publish/Resolve always match.
- Non-hermetic git: commit/tag inherited ambient config, so a global
commit.gpgsign=true (or a push credential prompt) could fail or hang a real
publish. Signing is now disabled per-invocation and git runs with
GIT_TERMINAL_PROMPT=0, while push credentials are preserved.
- Owner foot-gun: the default remoteFor closed over the constructor argument,
so reassigning GitHubStore.Owner silently published to the original owner.
It now reads s.Owner live.
Tests: digest ignores untracked/ignored files and tracks the exec bit;
Publish succeeds under a global config that forces signing with a bogus gpg
program; Owner reassignment retargets the remote.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… last
Second-pass review found three ways a publish could go wrong before or
after the irreversible push, all confirmed empirically:
- Path traversal: owner/name were interpolated into the remote URL
unvalidated, and git's HTTP client normalizes "owner/../../evil/repo"
to a different repository — a crafted library name could redirect a
credentialed publish. Both atoms are now validated as plain tokens at
every entry point, before any git command runs.
- Digest instability: treeDigest hashed working-tree bytes, so ambient
core.autocrlf (deliberately preserved for credentials) made the
publish-side digest differ from a resolve-side clone of the same
release — verification would flag legitimate content as tampered. The
digest is now over the index's blob object IDs, which are
content-addressed after git's own normalization and provably identical
on both sides (and require no file reads).
- Unfetchable releases: build-metadata semver ("1.0.0+build.1") parses,
tags, and pushes fine, but Go module versions discard build metadata,
so the immutable tag could never be `go get`-ed. Publish now refuses it.
Also: rev-parse/digest moved before the pushes so every failure leaves
the remote untouched and retryable; the verbatim tag name is carried
through resolution instead of being reconstructed from normalized
semver (a human-added "v1.0" no longer breaks Resolve); output() folds
git's stderr into errors instead of an opaque "exit status 128"; commit
runs --no-verify so ambient hooks cannot fail or mutate a release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 14caca8 into mainAug 23, 2026
5 checks passed
Sign up for freeto 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.

Epic: durable versioned library publishing — storage abstraction for go get / pip / npm

1 participant

@antoinetoussaint-byte