Conversation
|
🤖 Rebased The rebase had non-trivial conflicts because
Verification:
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
Replaces the local Env/OS/Map types and ConfigFromEnv() with cli/v3 flags scoped to the upload subcommand: --token BUILDKITE_ANALYTICS_TOKEN -> uploadConfig.SuiteToken --upload-url BUILDKITE_TEST_ENGINE_UPLOAD_URL -> uploadConfig.UploadUrl This brings upload into the same idiom as run, plan, and tools backfill-commit-metadata, where build/runtime configuration is bound through cli.EnvVars(...) rather than a custom env abstraction. RunEnvFromEnv now takes an EnvLookup function (matching the signature of os.LookupEnv directly), simplifying both the production call site (os.LookupEnv) and tests (a small mapLookup helper). Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
http.DefaultClient has no Timeout, so a stuck connection or slow response would block the upload indefinitely. Switch to a package http.Client with a 5 minute Timeout, which is generous for typical test result files but bounded enough to fail clearly. Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
Filename-extension inference is brittle: results files can have unconventional names (no extension, .junit, .results, .out, etc). --format junit --format json When --format is empty, behaviour is unchanged: format is inferred from .xml -> junit and .json -> json. Invalid formats produce a clear error rather than a server-side rejection. Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
Brings upload requests in line with internal/api: User-Agent: Buildkite Test Engine Client/<version> (<os>/<arch>) and adds an exponential-backoff retry loop (5 attempts, ~500ms base delay, jitter) for network errors, 429, and 5xx responses. The upload API has been seen returning transient 5xx, and previously the client would fail the whole job on the first hiccup. Body bytes are snapshotted up front so each retry attempt rebuilds a fresh request with a re-readable body. Per-attempt timeouts are still bounded by the package httpClient.Timeout (5 minutes). Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
Adds:
- TestUpload_RetriesOn5xxThenSucceeds: confirms transient 5xx
responses are retried and the eventual 202 is returned cleanly.
- TestUpload_DoesNotRetryOn4xx: confirms client errors short-circuit
the retry loop (no wasted retries on bad token / bad request).
- TestUpload_SetsUserAgent: confirms the bktec User-Agent header is
attached to upload requests.
- TestUploadFile_EndToEnd: covers the integration of token validation,
format inference, env-derived run metadata, and the HTTP send.
- TestUploadFile_MissingToken: confirms missing SuiteToken errors out
with the expected message.
- TestUploadFile_FormatOverride: confirms an explicit --format value
is used even when the filename has no usable extension.
- TestUploadFile_FormatInferenceFailsWithoutExtension: confirms the
helpful error when neither --format nor a recognised extension is
available.
Also fixes createTestXML to produce filenames ending in .xml (the
previous os.CreateTemp("", "test.xml") put the random suffix
*after* the extension, hiding format inference).
Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895
Co-authored-by: Amp <amp@ampcode.com>
The previous "file does not exist: %s" message obscured permission errors, EACCES on parent dirs, EIO, etc. Wrapping with %w preserves errors.Is(err, fs.ErrNotExist) for callers that want to distinguish missing files from other failure modes. Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
Replaces the EnvLookup function-typed parameter with a typed BuildEnv struct populated from cli flags, sharing the existing buildIDFlag, branchFlag, and jobIDFlag with bktec run / plan, and adding commitFlag, messageFlag, buildNumberFlag, buildUrlFlag bound to new config.Config fields (Commit, Message, BuildNumber, BuildUrl). The upload package no longer touches os.Environ at all: env access is fully consolidated in cli.go where every other command reads its inputs. RunEnvFromEnv is replaced with RunEnvFromBuildEnv that takes the typed struct directly, which also makes the Buildkite-vs-generic detection more obvious (build.BuildId != ""). Amp-Thread-ID: https://ampcode.com/threads/T-019dd7a1-d422-717e-aac1-a48f10021895 Co-authored-by: Amp <amp@ampcode.com>
|
🤖 Implemented the eight review suggestions from a local amp/Opus review as separate commits, all pushed to
Verification: |
|
🤖 Cumulative review of The feature is in good shape — Higher impact1. Upload-only fields on
|
internal/api |
new upload |
|
|---|---|---|
| attempt cap | TryForever() |
WithMaxAttempts(5) |
| total budget | 130s context wrapping the loop | none — bounded only by http.Client.Timeout × 5 |
| per-attempt deadline | 15s context.WithTimeout |
none (5min client timeout) |
Worst-case the upload retry loop can sit for ~25 minutes on a hung server with a context.Background() parent, vs. ~2 minutes for internal/api. The new policy probably is the right policy for uploads (15s is too short for big bodies), but I'd either:
- soften/remove the "matches the behaviour of the internal/api client" wording, or
- factor an explicit shared retry-classification helper and document the deliberate differences.
6. Excess exported surface in internal/upload
Only the CLI wiring needs UploadFile, BuildEnv, and Config. These can be unexported:
Upload(covered byUploadFile; tests live in the same package)RunEnvFromBuildEnvRunEnvMapMultipartBodyand all four methodsNewMultipartBody
Tests are in package upload so unexporting won't break them. Smaller surface = clearer contract = easier to split the package later (see #3).
7. --token is ambiguous next to existing --access-token
bktec upload --token … is a suite token (analytics ingest), while every other Test Engine call uses --access-token (Buildkite API token). Consider --suite-token or --analytics-token (env stays BUILDKITE_ANALYTICS_TOKEN). The current error string is also a bit terse:
return fmt.Errorf("BUILDKITE_ANALYTICS_TOKEN missing")
Compare with the existing pattern: "--suite-token / BUILDKITE_ANALYTICS_TOKEN must not be blank".
8. Response key inconsistency between code and tests
TestUploadserver returns{"id":"theuuid","url":"..."}TestUpload_RetriesOn5xxThenSucceedsreturns{"upload_url":"..."}- Production code logs
respData["upload_url"]
Pick one and match the real API. A typed response struct (type uploadResponse struct { ID, URL string } or whatever the API actually returns) is preferable to the current map[string]string dance, and would have caught this drift.
Lower impact / cleanup
9. Validation for upload is split across CLI arg checks and lower-level code
Consider a small cfg.ValidateForUpload() (or local equivalent on the new upload-local struct) covering: token present, exactly one positional arg, format valid if set, upload-url parseable. Keeps the validation pattern consistent with ValidateForRun/ValidateForPlan/ValidateForBackfillCommitMetadata.
10. MultipartBody.writer stored by value
type MultipartBody struct {
writer multipart.Writer // not *multipart.Writer
buf *bytes.Buffer
}
…
writer: *multipart.NewWriter(buf),Currently safe (multipart.Writer is constructed once and not copied again), but *multipart.Writer is the documented type and removes any "did this get copied?" questions. Trivial change.
11. Whole body buffered in memory, then snapshot for retries
Fine for typical JUnit/JSON sizes, and the snapshot via body.buf.Bytes() is safe because nothing mutates the buffer after Close. Worth noting in a comment so a future change to MultipartBody doesn't accidentally break retry safety. If very large result files become a real use case, switch to a temp-file-backed body with req.GetBody.
12. Cosmetic
userAgentis computed at package init — fine, butinternal/apibuilds it per-request insideRoundTrip. Either is OK; minor inconsistency.--upload-urlbeingHidden: trueis consistent with--base-urlin this repo. ✓cfg.Output,cfg.UploadFile,cfg.Days, etc. all havejson:"-"to keep them out of the env payload — same treatment for Copy over existing project from Buildkite #1 if you go that route.- No
docs/upload.md— every other runner/feature has a docs page; worth adding for parity (separate PR is fine).
What looks good
- Token never logged; only key/format/filename/url go into log lines.
- 5-min
http.Client.Timeoutis a reasonable default and covers the "DefaultClient waits forever" footgun. - Retry classification is correct: 4xx breaks (
r.Break()+ return non-nil err short-circuits the loop), 429/5xx/network err retry, body bytes are re-readable on each attempt. errors.Is(err, fs.ErrNotExist)test for the wrappedos.Staterror is a nice touch.TestUpload_DoesNotRetryOn4xxandTestUpload_SetsUserAgentare exactly the right shape.- BuildEnv field-by-field copy in
main.gois slightly stuttery but a deliberate firewall betweeninternal/uploadandinternal/config— happy to keep that boundary.
Minimum changes I'd ask for before merge
- get the upload-only fields off
config.Config(orjson:"-"them) sorun/planpayloads don't change shape. filepath.Basefor the multipart filename.debug.SetOutput(os.Stderr)inuploadAction, and dropslog.
Everything else is cleanup or follow-up — feel free to defer.
This is something we should fix outside this PR, I think. It's unusual to embed a local config struct into an API payload, unless it's specifically a usage telemetry request. |
Teach
bktecto upload test results to Test Engine.