Repository navigation
feat: Artifact service (@actions/artifact v2 parity) [needs live validation] - #126
Conversation
052249f to
b838cf6
Compare
1946c25 to
f80e124
Compare
There was a problem hiding this comment.
Code Review
Current Commit: f80e124
Validation Results
| Check | Status |
|---|---|
| PR Title Validation | ✅ success |
| Conventional Commits | ✅ success |
| Code Quality | ✅ success |
| Markdown | ✅ success |
| Build | ✅ success |
| Tests | ✅ success |
| CodeQL | ✅ success (no new alerts) |
All checks green. This is a first review — no previous inline comments to resolve.
Summary
Solid draft. The protocol research is thorough (the version: 7 / SHA-256 corrections over the spec's guesses are valuable), the twirp.ts extraction is clean, Redacted token handling and Effect.acquireUseRelease resource cleanup are both done right, and the defensive camelCase+snake_case dual-field reading is exactly the right call for a reverse-engineered protocol. Test coverage is comprehensive for a fully mocked implementation — backendIds.ts in particular is well-isolated and tested.
Four items to address before the live-validation pass:
1. Duplicate isRetryableReason [fix before merge]
ArtifactLive.ts:94–97 defines a local isRetryableReason that is byte-for-byte identical to isRetryableTwirpReason already exported from internal/twirp.ts. The entire point of extracting twirp.ts was to share this predicate. Import and use isRetryableTwirpReason instead — see inline comment.
2. FinalizeArtifactResponse reused for DeleteArtifact [nit]
The DeleteArtifact RPC is called with twirpCall<FinalizeArtifactResponse> which works today because both responses happen to share { ok, artifactId }, but silently couples two unrelated protocol operations. A dedicated DeleteArtifactResponse type is cheap insurance. See inline.
3. Azure upload constants duplicated [pre-merge suggestion]
UPLOAD_CHUNK_SIZE, UPLOAD_CONCURRENCY, and UPLOAD_MAX_SINGLE_SHOT are defined identically in both ActionCacheLive.ts and ArtifactLive.ts. If these get tuned after the live-validation run (e.g. chunking strategy differs for artifacts vs cache), the duplication will bite. A shared internal/azureUpload.ts or just re-exporting them from internal/twirp.ts would consolidate them. See inline.
4. Missing test: empty files array [nit]
createZip has an early throw for files.length === 0 but it's the only error path in uploadArtifact without a test. One-liner to add. See inline.
Positives worth noting
- The JWT
scp-claim parsing inbackendIds.tsis well-isolated with its own test file — easy to update if the token format changes. getArtifactEnvredacting the token immediately and unwrapping it only at the request boundary (S9) is exactly right.Effect.acquireUseReleasefor both upload (zip creation → cleanup) and download (zip download → cleanup) correctly handles failure cleanup without try/finally boilerplate.- The
CONFLICTsentinel returning[]fromlistInternal(line 305) is a graceful fallback over failing on a protocol edge case. findByUnsupportedas a named helper makes the fourfindBystubs easy to scan and update together when the REST path gets implemented.- The
twirp.test.tsretry-schedule test usingTestClockis a nice touch — it validates the retry budget without wallclock waits.
fb69473 to
cec7d30
Compare
Code ReviewCurrent Commit:
CI Checks
Resolved Since Last Review
Open Issues1.
|
cec7d30 to
4c1dc37
Compare
| * zlib compression level 0–9 passed to `zip`. Default 6, matching | ||
| * `@actions/artifact`. | ||
| */ | ||
| readonly compressionLevel?: number; |
There was a problem hiding this comment.
compressionLevel is documented as "zlib compression level 0–9 passed to zip" but createZip neither accepts nor uses it — the zip call at ArtifactLive.ts:190 always uses the default level. Users setting this option today get no effect.
POSIX zip supports -0 through -9 (e.g. -6 is the default, matching @actions/artifact). The fix is:
// createZip signature:
const createZip = (files, rootDir, artifact, compressionLevel = 6) => ...
// POSIX branch:
execFileSync("zip", [`-${compressionLevel}qr`, zipPath, ...relFiles], { cwd: rootDirectory, stdio: "pipe" });Worth implementing or explicitly marking as deferred (e.g. // TODO: compressionLevel not yet wired) before the public API solidifies — right now the JSDoc promise is silently broken.
| }), | ||
| ); | ||
| } | ||
| const id = Number(pickArtifactId(deleteResult) ?? match.value.id); |
There was a problem hiding this comment.
pickArtifactId is typed (r: FinalizeArtifactResponse) but is called here with deleteResult (DeleteArtifactResponse). TypeScript accepts it because the two interfaces are structurally identical today, but it undermines the comment at lines 112–115 ("avoid coupling the two if either diverges").
One-liner fix — add a dedicated picker:
const pickDeleteArtifactId = (r: DeleteArtifactResponse): string | undefined => r.artifactId ?? r.artifact_id;…and use it here:
const id = Number(pickDeleteArtifactId(deleteResult) ?? match.value.id);Add an Artifact service (uploadArtifact, listArtifacts, getArtifact, downloadArtifact, deleteArtifact) reverse-engineered from the actions/toolkit v2 backend: Twirp ArtifactService over HttpClient plus Azure block-blob transfer, with run/job backend IDs decoded from the ACTIONS_RUNTIME_TOKEN scp claim. The finalize hash is SHA-256 hex and the create version is 7, both confirmed against the toolkit source. Zips shell out to zip/unzip with a PowerShell fallback on Windows, preserving the zero-CJS posture. Extract the shared Twirp client and retry schedule from ActionCacheLive into internal/twirp.ts; ActionCacheLive tests are untouched and stay green. Adds ArtifactError and an in-memory ArtifactTest layer. Cross-run findBy and several protocol details remain to be validated on a live runner. Signed-off-by: C. Spencer Beggs <spencer@savvyweb.systems>
4c1dc37 to
55ebd35
Compare
WS4 (part 2) —⚠️ DRAFT: needs live-runner validation
ArtifactservicePart 2 of WS4 (PR-C from the spec). The largest
@actions/*coverage hole. Bump: minor.What's implemented
uploadArtifact/listArtifacts/getArtifact(→Option) /downloadArtifact/deleteArtifact, plusArtifactError(withretryable) and an in-memoryArtifactTest. Backend: Twirpgithub.actions.results.api.v1.ArtifactServiceover@effect/platformHttpClient+ Azure block-blob, with run/job backend IDs decoded from theACTIONS_RUNTIME_TOKENscpclaim. Zips shell out tozip/unzip(PowerShell fallback on Windows) — zero new deps.Protocol questions resolved by reading the toolkit source (corrects the spec)
sha256:<hex>), NOT CRC64 —blob-upload.ts/upload-artifact.tsusecrypto.createHash('sha256'). Nocrc64module needed.CreateArtifactversionis7, not the spec's guessed4.Actions.Results:<run>:<job>fromscp) replicated fromshared/util.ts.Refactor (regression-fenced)
Shared Twirp plumbing extracted from
ActionCacheLiveintointernal/twirp.ts(parameterized by service segment).ActionCacheLive.test.tsis byte-for-byte untouched and green; the retry-keying substrings (HTTP <status>,ECONNRESET) survive the extraction.hashcontract over the exact uploaded bytes.version: 7acceptance / whether it's required.ok:false— both handled).findBy(cross-run/cross-repo REST path) is a typed stub — implement viaGitHubClientif needed.Verification (mocked; no live backend hit)
pnpm run lint— exit 0 ·pnpm run ci:build— passes ·pnpm run test— 1157 passed (Azure SDK + TwirpHttpClientfully mocked;ArtifactLiveWindows-only PowerShell zip branches are the only uncovered lines on this darwin host)Signed-off-by: C. Spencer Beggs spencer@savvyweb.systems