Skip to content

Add rwx package build to build and upload packages - #596

Draft
dan-manges wants to merge 3 commits into
mainfrom
dan/package-build
Draft

Add rwx package build to build and upload packages#596
dan-manges wants to merge 3 commits into
mainfrom
dan/package-build

Conversation

@dan-manges

Copy link
Copy Markdown
Member

Implements rwx package build, which zips a directory and uploads it to the RWX package registry, printing the content digest returned by the server.

rwx package build [directory] [--timestamp YYYYMMDDHHmm]

Ports the logic from rwx-cloud/packages#338.

Timestamps are optional and applied inline

The reference implementation shells out to find "$DIRECTORY" -exec touch -t "$TIMESTAMP" {} \; before zipping. This version instead sets each entry's modification time directly in the zip header via archive/zip, so the source tree is never mutated.

  • --timestamp is optional. Omitting it preserves each file's real mtime.
  • Entries are written in sorted order, so the same tree and timestamp always produce identical archive bytes.

Changes

File Change
internal/api/config.go UploadPackageConfig / UploadPackageResult{Digest}
internal/api/client.go Client.UploadPackage — multipart POST /mint/api/leaves, form field file; non-2xx surfaces the response body
internal/cli/interfaces.go, internal/mocks/api.go Interface + mock plumbing
internal/cli/service_package_build.go Service.BuildPackage + deterministic zipDirectory
cmd/rwx/packages.go build subcommand, --timestamp flag, package alias on the parent

Contents land at the archive root (not nested under the directory name), matching cd dir && zip -r … .. Exec bits are preserved and symlinks are followed, matching zip without -y.

rwx package build works via a package alias on the existing packages group, so rwx packages build works too. The group's existing Hidden setting was left as-is.

Verification

go build ./..., go vet, and golangci-lint run ./... are clean, and 17 new tests cover the three layers.

Verified live against the API:

# with --timestamp, before and after mutating every mtime on disk
Uploaded package with digest: 774dd467c210d622bffe18fe071c3ab2c97850be5f591f93350fdebe5242f85f
Uploaded package with digest: 774dd467c210d622bffe18fe071c3ab2c97850be5f591f93350fdebe5242f85f

# without --timestamp, before and after mutating every mtime on disk
Uploaded package with digest: 3a0c3206102d9a3444707ac1e292e7498f9b06d87cc3933f5937c7a830ab0d44
Uploaded package with digest: feb951c0951dca4214b6f917ba6bfb9b7cdfd43f77117a1f9458154691c491c4

An earlier attempt returned mint-leaf.yml has a leaf name owned by 'rwx-cli-smoke', but request was made under organization 'rwx', confirming the server parses the uploaded archive layout correctly.

For review

  • Digest is not byte-compatible with the shell package. Go's zip writer differs from Info-ZIP, so the same source yields a different digest than rwx/package produces. Stable and correct, but a one-time cache miss for anything moving between the two.
  • UTC vs. local time. touch -t treats the timestamp as local wall-clock; this parses it as UTC so digests don't vary by machine timezone. Worth confirming that's desired.
  • Build only, no publish. Matches Fix sandbox pull erasing local uncommitted changes #338; /mint/api/leaves/publish is deliberately out of scope.
  • The archive is buffered in memory — fine for packages, not for very large trees.

ampagent and others added 3 commits August 10, 2026 19:51
Zips the contents of a directory and uploads it to the RWX package
registry, printing the content digest returned by the server.

    rwx package build [directory]

Ports the logic from rwx-cloud/packages#338, with one deliberate
difference: timestamp normalization is optional and applied inline to
the zip entry headers rather than by running `touch` over the source
tree. Passing `--timestamp YYYYMMDDHHmm` sets every entry's modification
time in the archive, so builds are reproducible for content-based
caching without mutating files on disk. Omitting it preserves each
file's real mtime.

Archive entries are written in sorted order so the same tree and
timestamp always produce identical bytes.

Co-authored-by: Dan Manges <dan@rwx.com>
Adds .rwx/integration/package-build-test.yml, which builds the real CLI
and exercises `rwx package build` against the package registry, and
registers it in .rwx/integration.yml so it runs on every pull request.

Covers the happy path, digest reproducibility with --timestamp across
mtime rewrites, mtime sensitivity without --timestamp, and that each
server-side validation error is displayed to the user: missing
rwx-package.yml (including the multi-line file listing), missing
README.md, invalid YAML, a manifest with no name, a non-semver version,
a name owned by another organization, and an unauthorized token.

Also routes UploadPackage through decodeResponseJSON so 401 and 5xx
responses are wrapped with the ErrUnauthenticated / ErrInternalServerError
sentinels instead of classifying as `unknown` in telemetry. The unit
tests now assert against error bodies captured verbatim from
cloud.rwx.com.

Co-authored-by: Dan Manges <dan@rwx.com>
rwx-cloud/cloud#8414 turns several unhandled package upload inputs into
400 validation responses and adds archive limits. All of them keep the
{"error": "<single string>"} contract, so the CLI displays them as-is.

Adds unit coverage for the new messages: missing description, manifest
that is not a mapping, invalid parameters, missing/unreadable file, not
a readable zip, the compressed, uncompressed and file count limits, and
the already-published error.

Manifest-scoped messages now name whichever file was uploaded, so the
integration assertions stay filename-agnostic and pass against both the
current and the updated server.

Co-authored-by: Dan Manges <dan@rwx.com>
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.

2 participants