Skip to content

Add basecamp files replace: publish a new version of an uploaded file - #634

Merged
jeremy merged 7 commits into
mainfrom
feat/files-replace
Aug 12, 2026
Merged

Add basecamp files replace: publish a new version of an uploaded file#634
jeremy merged 7 commits into
mainfrom
feat/files-replace

Conversation

@jeremy

@jeremyjeremy commented Aug 12, 2026

Copy link
Copy Markdown
Member

Closes#404 — the write half of the upload-versions story. #622's files versions reads the history; files replace <upload-id|url> <file> writes to
it, over SDK v0.14.0's UploadsService.CreateVersion: stage the file as an
attachment (the same two-step flow as uploads create), then POST it to the
upload's versions endpoint.

The upload keeps its id, URL and comments; the previous file becomes a past
version. That's the issue's use case verbatim: a release script ships each
build to the same published link instead of minting duplicates. (#404
proposed PUT-with-sgid on the upload itself — bc3 has since shipped the
dedicated versions endpoint, which is what this rides.)

Semantics follow the endpoint's presence rules:

  • nobody is notified (no notify on the wire)
  • the description carries forward unless --description is given; an
    explicit --description "" clears it (tri-state pinned by
    TestFilesReplaceDescriptionTriState, including that omission stays off
    the wire)
  • --base-name renames without touching the extension
  • alias: files new-version

Completeness bar: catalog action, API-COVERAGE row, SKILL.md examples +
semantics note, .surface regenerated (406 lines — command + alias across
the tree), unit tests (two-step flow order, URL acceptance, tri-state), a
real write smoke (files replace on the suite's own created upload, then
files versions asserting exactly one current) plus out-of-scope markers for
the alias leaves.

Live-verified against production: uploads createfiles replace
files versions shows both filenames with exactly one current: true, same
upload id throughout. Verification upload trashed after.

bin/ci green at the head (real exit 0).


Summary by cubic

Add basecamp files replace to publish a new version of an uploaded file while keeping the same upload ID/URL and comments. This delivers the write half of #404 and pairs with files versions to support stable links for new builds.

  • New Features

    • Command: basecamp files replace <upload-id|url> <file> (alias: new-version) via SDK UploadsService.CreateVersion (stage attachment, then POST to versions).
    • Behavior: no notifications; description carries forward unless --description is set (empty clears); --base-name renames without changing the extension.
    • Accepts an upload ID or full Basecamp URL; returns the same upload with the new current version.
    • Updated command catalog/help; unit and e2e smoke tests; API coverage docs include replace.
  • Bug Fixes

    • Pre-flight validation: reject URLs on untrusted hosts, URLs for a different account, non-positive upload IDs, wrong recording types, uploads collection/listing URLs (must target a single upload), and unresolved local images in --description before staging any data.
    • Breadcrumbs: reuse the caller’s reference; include --project with fallback to the root-level --project when the group flag is unset; shell-quote all embedded values (both files replace and files versions) so emitted commands are safe and round-trip through a shell.
    • Smoke test: read JSON from $output directly; tests pin --base-name presence/omission behavior.

Written for commit a0a5ec2. Summary will update on new commits.

Review in cubic

Closes the write half of the upload-versions story (#404): files versions
(#622) reads the history, files replace writes to it, over the SDK v0.14.0
CreateVersion method — stage the file as an attachment, then POST it to the
upload's versions endpoint. The upload keeps its id, URL and comments; the
previous file becomes a past version, which is the point: a release script
can ship each build to the same published link instead of a new duplicate.
Defaults follow the endpoint's presence semantics: nobody is notified, and
the description carries forward unless --description is given (an explicit
empty string clears it). --base-name renames without touching the extension.
The issue predates the dedicated endpoint — it proposed PUT-with-sgid on the
upload itself, which bc3 has since superseded with POST /versions.json.
Live-verified against production: uploads create -> files replace -> files
versions shows both filenames with exactly one current version; the
verification upload was trashed after.
CopilotAI balanced review requested due to automatic review settings August 12, 2026 01:10
@github-actionsgithub-actionsBot added commands CLI command implementations tests Tests (unit and e2e) skills Agent skills docs labels Aug 12, 2026

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds files replace to publish new upload versions while preserving the upload’s identity and history.

Changes:

  • Implements attachment staging and version creation with description and filename options.
  • Adds unit and smoke coverage.
  • Updates CLI metadata, documentation, API coverage, and surface snapshots.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
FileDescription
internal/commands/files.goImplements and registers files replace.
internal/commands/files_test.goTests replacement flow, URLs, and descriptions.
internal/commands/commands.goAdds the catalog action.
e2e/smoke/smoke_files_write.batsAdds the live replacement smoke test.
e2e/smoke/smoke_lifecycle.batsMarks alias paths out of scope.
skills/basecamp/SKILL.mdDocuments usage and semantics.
API-COVERAGE.mdRecords endpoint coverage.
.surfaceRegenerates the CLI surface snapshot.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threade2e/smoke/smoke_files_write.bats Outdated
Comment threadinternal/commands/files.go Outdated
Comment threadinternal/commands/files.go

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:fe92090ed7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadinternal/commands/files.go
Comment threadinternal/commands/files.go Outdated
Comment threadinternal/commands/files.go
Comment threade2e/smoke/smoke_files_write.bats Outdated
…overage
- the smoke test's current-version count went through bash -c with API JSON
interpolated into single quotes — an apostrophe in a creator name breaks
it, and worse could execute; assert_json_value reads $output directly
- replace's breadcrumbs now reuse the caller's reference and carry
--project, same contract as versions' (download resolves a project first)
- --base-name is pinned both ways: present it travels as base_name, omitted
it stays off the wire
CopilotAI review requested due to automatic review settings August 12, 2026 01:17

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (1)

internal/commands/files.go:1755

  • --project accepts project names, so interpolating it directly makes both breadcrumbs invalid for values such as "My Project" (--project My Project becomes two arguments). ref is also the original URL and may contain shell metacharacters such as &. Shell-quote both dynamic arguments, or construct the hints from resolved numeric IDs, before embedding them in copyable commands.
 ref := args[0]
scope := ""
if *project != "" {
scope = fmt.Sprintf(" --project %s", *project)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:3bf8723b01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadinternal/commands/files.go Outdated
Comment threadinternal/commands/files.go Outdated
…crumb scopes
- a pasted URL naming a different account is refused up front: extractID
keeps only the number, which would silently retarget the session
account's same-numbered upload on a mutating request
- upload IDs must be positive before anything is transferred
- the description's local image references resolve before the replacement
blob is staged, so a deterministic local failure no longer costs the
whole transfer
- breadcrumb --project values with whitespace are quoted (both replace and
versions), so emitted commands round-trip through a shell
All four pinned by TestFilesReplaceRejectsBeforeStaging (zero requests on
the wire) and the existing breadcrumb tests.
CopilotAI review requested due to automatic review settings August 12, 2026 01:26

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

internal/commands/files.go:1801

  • Go %q is not shell escaping: unquoted project names can contain ;, $(), or backticks, and the double quotes used for whitespace still allow command substitution. Because these breadcrumbs are presented as copy-pasteable shell commands, always POSIX single-quote the project value (matching config.ShellQuote) rather than only checking for whitespace.
	if strings.ContainsAny(project, " \t") {
return fmt.Sprintf(" --project %q", project)
}
return " --project " + project

Comment threadinternal/commands/files.go Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:99813373d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadinternal/commands/files.go Outdated
Comment threadinternal/commands/files.go Outdated
Third review round on the same two classes — the instrument was wrong, not
under-tuned:
- Breadcrumbs now pass every embedded value (the caller's reference AND the
project scope) through shellQuote: inert strings bare, everything else
single-quoted, the one POSIX form in which nothing substitutes. An
encoding at the boundary instead of another metacharacter case.
- A URL argument must identify an UPLOAD in the session's account: account
and path type both validated before anything stages, so a same-numbered
todo URL can no longer retarget the mutation.
TestShellQuote pins the encoding (including query-string URLs, embedded
single quotes and command substitution); the wrong-type URL joins
TestFilesReplaceRejectsBeforeStaging's zero-requests table.
CopilotAI review requested due to automatic review settings August 12, 2026 01:34
@jeremy

Copy link
Copy Markdown
MemberAuthor

Review-loop note, for the record: this was the third round on the same two
classes (breadcrumb quoting: missing scope → whitespace → metacharacters; URL
identity: account → path type). Per the working agreement, a third variation
means reassess the instrument rather than write the next selector — and the
reassessment concluded both classes close structurally in fe53313:

  • Quoting is now an encoding, not an escape list. Every value embedded in
    breadcrumb text (the caller's reference and the project scope, in both
    replace and versions) passes through shellQuote: clearly-inert strings
    stay bare, everything else is single-quoted — the one POSIX form in which
    nothing substitutes. There is no remaining metacharacter dimension to find.
  • A URL argument must identify an upload in the session's account. Account
    and path type are both validated before anything stages. Together they pin
    the URL to exactly the resource class this command mutates; there is no
    remaining URL dimension that can retarget it.

If a fourth variation of either class appears, it's evidence the closure
reasoning above is wrong somewhere — treat it as a bug in this note, not as
the next patch.

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

internal/commands/files.go:1805

  • The documented POSIX escape sequence is malformed: it ends with a curly double quote and does not match the '\'' sequence emitted below. Correct the example so future changes do not copy an invalid quoting form.
// spelled '\”. This is an encoding applied to every embedded value, not a

Comment threadinternal/commands/files.go
Copilot found the hole the class-closure note promised to treat as a bug in
the closure reasoning: collection URLs (/vaults/456/uploads,
/buckets/456/uploads) parse as type uploads while their extracted ID is the
parent's, so the type-and-account guard alone could still retarget a
same-numbered upload. The predicate now requires a non-collection URL with a
recording ID — type, account, and identity, all three. Pinned by the
vault-scoped collection case in TestFilesReplaceRejectsBeforeStaging.
Also gofmt on the TestShellQuote table, which is what CI's lint tripped on.
CopilotAI review requested due to automatic review settings August 12, 2026 01:41

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

internal/commands/files.go:1812

  • The documented POSIX representation for an embedded single quote is malformed here; it should match the '\'' sequence implemented below so this security-sensitive explanation remains accurate.
// spelled '\”. This is an encoding applied to every embedded value, not a

internal/commands/files.go:1739

  • resolveLocalImages validates and uploads each image in one reverse-order pass. With a description such as a missing image followed by a valid local image, it uploads the valid image first and only then returns the local validation error, leaving staged data even though replacement never runs. This contradicts the preflight/no-request contract documented by this change. Validate every local image before uploading any of them (and add a mixed valid/missing regression case), then perform resolution.
 if descHTML, err = resolveLocalImages(cmd, app, descHTML); err != nil {

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:cc7a0a094c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadinternal/commands/files.go Outdated
The root command binds --project separately (app.Flags.Project); the
breadcrumb scope only consulted the files-group flag, so a root-level scope
vanished from the emitted commands. scopeProject mirrors the fetch paths'
precedence — group flag, then root flag, deliberately not the config
default, which a copier's own config supplies. Applied to both replace and
versions; pinned by TestScopeProject.
CopilotAI review requested due to automatic review settings August 12, 2026 01:49

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

internal/commands/files.go:1812

  • The documented POSIX single-quote escape is malformed; write \'\'' so the comment accurately matches the replacement performed below.
// spelled '\”. This is an encoding applied to every embedded value, not a

Comment threadinternal/commands/files.go

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:16211fa680

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment threadinternal/commands/files.go
Comment threadinternal/commands/files.go
Two adoptions of controls the repo already had, at call sites that lacked
them:
- A URL-shaped argument must pass hostutil.IsTrustedBasecampHost before the
identity checks: the URL router is host-agnostic, so a look-alike on an
attacker-controlled host carrying the configured account's own IDs passed
account/type/identity and would have retargeted a real upload — the
confused-deputy case the comment and chat commands already gate.
- resolveLocalImages validates every image reference before uploading any:
processing in reverse meant a description with a missing image first and a
valid image later uploaded the valid one, then failed, stranding the
attachment. Helper-level fix, so every caller gets it.
Both pinned in TestFilesReplaceRejectsBeforeStaging (zero requests); the
mixed-image case is red-proven — against the pre-fix helper it fails with a
staged upload on the wire.
CopilotAI review requested due to automatic review settings August 12, 2026 01:57

CopilotAI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (1)

internal/commands/files.go:1822

  • The shell-escaping example ends with a curly double quote and does not show the sequence that shellQuote actually emits. Correct it to '\'' so the helper's documentation accurately explains embedded single-quote handling.
// spelled '\”. This is an encoding applied to every embedded value, not a

@jeremy
jeremy merged commit c96ecf0 into mainAug 12, 2026
25 checks passed
@jeremy
jeremy deleted the feat/files-replace branch August 12, 2026 02:03
jeremy added a commit that referenced this pull request Aug 22, 2026
…or-roundtrip
* origin/main: (96 commits)
ci: bump the github-actions group with 6 updates (#639)
Reject three more doomed invocations before draining stdin (#645)
Stdin `-` support everywhere sensible; usage error for stray `-` elsewhere (#641)
Add hey-cli Windows signing secrets to the release env manifest (#642)
deps: bump the go-dependencies group with 5 updates (#638)
Update nix flake and plugin version for v0.9.1
ci: bump the github-actions group with 4 updates (#633)
Add basecamp files replace: publish a new version of an uploaded file (#634)
Add basecamp files versions — HELD, blocked on the SDK (#622)
Update nix flake and plugin version for v0.9.0
Make the Codex probe's timeout actually bound doctor (#629)
Make the lockstep check catch stale agreement and .yaml workflows (#628)
Keep refreshing opencode's other spelling (#627)
Lint the release the same way we lint everything else (#625)
Install the skill where opencode actually looks (#624)
Take the communiques out of the source tree (#623)
Correct the API coverage claim: 183/184, not 100% (#621)
Stop echoing back step fields the caller never changed (#620)
Drive the circuit breaker's clock from tests, not sleep() (#619)
Tell agents the truth about card column moves (#618)
...
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commandsCLI command implementationsdocsskillsAgent skillstestsTests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support uploading a new version of an existing file

2 participants

@jeremy