From 0636509bc682cb9ea86a7313d2fe5a45f9e685a2 Mon Sep 17 00:00:00 2001 From: Krishna Date: Wed, 17 Jun 2026 19:40:51 -0700 Subject: [PATCH 1/8] Fixing the vercel_artifacts backend --- core/services/vercel-artifacts/src/backend.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/services/vercel-artifacts/src/backend.rs b/core/services/vercel-artifacts/src/backend.rs index 5d0aa6533631..9eda4d14d839 100644 --- a/core/services/vercel-artifacts/src/backend.rs +++ b/core/services/vercel-artifacts/src/backend.rs @@ -112,6 +112,10 @@ impl Service for VercelArtifactsBackend { } async fn stat(&self, ctx: &OperationContext, path: &str, _args: OpStat) -> Result { + if path == "/" || path.ends_with('/') { + return Ok(RpStat::new(Metadata::new(EntryMode::DIR))); + } + let response = self.core.vercel_artifacts_stat(ctx, path).await?; let status = response.status(); From 384276fbdfa7d2dfa284dd3592dfde8c5648265a Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Mon, 29 Jun 2026 20:08:02 +0000 Subject: [PATCH 2/8] docs: add limitations section and inline comments for vercel_artifacts CAS constraints Explain why create_dir, delete, and list are unsupported: Vercel Remote Cache is content-addressable storage and does not expose folder operations or cache deletion via its API. Add a Limitations section to docs.md and inline comments to the relevant backend methods. Addresses reviewer feedback on #7793. --- core/services/vercel-artifacts/src/backend.rs | 2 ++ core/services/vercel-artifacts/src/docs.md | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/core/services/vercel-artifacts/src/backend.rs b/core/services/vercel-artifacts/src/backend.rs index 9eda4d14d839..f2c44c01486b 100644 --- a/core/services/vercel-artifacts/src/backend.rs +++ b/core/services/vercel-artifacts/src/backend.rs @@ -105,6 +105,7 @@ impl Service for VercelArtifactsBackend { _path: &str, _args: OpCreateDir, ) -> Result { + // Vercel Remote Cache is content-addressable storage (CAS) and does not support folder operations. Err(Error::new( ErrorKind::Unsupported, "operation is not supported", @@ -156,6 +157,7 @@ impl Service for VercelArtifactsBackend { } fn delete(&self, _ctx: &OperationContext) -> Result { + // Vercel Remote Cache is content-addressable storage (CAS) and does not support resource deletion. Err(Error::new( ErrorKind::Unsupported, "operation is not supported", diff --git a/core/services/vercel-artifacts/src/docs.md b/core/services/vercel-artifacts/src/docs.md index 9a359a3fa6b0..b531ee1fcf52 100644 --- a/core/services/vercel-artifacts/src/docs.md +++ b/core/services/vercel-artifacts/src/docs.md @@ -12,6 +12,12 @@ This service can be used to: - [ ] ~~rename~~ - [ ] ~~presign~~ +## Limitations + +Vercel Remote Cache is a Content-Addressable Storage (CAS) designed for caching build artifacts. Because of this, it has the following limitations: +- **Folder Operations**: It does not support creating directories (`create_dir`) or listing files (`list`). +- **Resource Deletion**: It does not support deleting individual remote cache artifacts (`delete`). Standard cache invalidation is managed automatically by Vercel or triggered locally via cache misses (by changing task hashes). + ## Configuration - `access_token`: set the access_token for Rest API From 26c033508c97c97ba6e0e869ff3b679c53145759 Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Tue, 30 Jun 2026 19:39:11 -0700 Subject: [PATCH 3/8] fix(vercel-artifacts): behavior test readiness - Add .github/services/vercel_artifacts/default/action.yml to wire vercel_artifacts into the behavior test CI - Remove unverified read_with_suffix from Capability - Guard write() against directory paths (IsADirectory) - Document limitations in docs.md --- .../vercel_artifacts/default/action.yml | 30 +++++++++++++++++++ core/services/vercel-artifacts/src/backend.rs | 9 +++++- core/services/vercel-artifacts/src/docs.md | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/services/vercel_artifacts/default/action.yml diff --git a/.github/services/vercel_artifacts/default/action.yml b/.github/services/vercel_artifacts/default/action.yml new file mode 100644 index 000000000000..9aa51cab7ef3 --- /dev/null +++ b/.github/services/vercel_artifacts/default/action.yml @@ -0,0 +1,30 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: default +description: 'Behavior test for Vercel Remote Cache' + +runs: + using: "composite" + steps: + - name: Setup + uses: 1Password/load-secrets-action@92467eb28f72e8255933372f1e0707c567ce2259 # v4.0.0 + with: + export-env: true + env: + OPENDAL_VERCEL_ARTIFACTS_ACCESS_TOKEN: op://services/vercel_artifacts/access_token + OPENDAL_VERCEL_ARTIFACTS_TEAM_ID: op://services/vercel_artifacts/team_id diff --git a/core/services/vercel-artifacts/src/backend.rs b/core/services/vercel-artifacts/src/backend.rs index e0a084ddc2bd..2a6f1790a7d6 100644 --- a/core/services/vercel-artifacts/src/backend.rs +++ b/core/services/vercel-artifacts/src/backend.rs @@ -96,7 +96,6 @@ impl Builder for VercelArtifactsBuilder { stat: true, read: true, - read_with_suffix: true, write: true, @@ -205,6 +204,14 @@ impl Service for VercelArtifactsBackend { } fn write(&self, ctx: &OperationContext, path: &str, args: OpWrite) -> Result { + // Vercel Remote Cache is key-based; paths ending with '/' are treated as directories. + if path.ends_with('/') { + return Err(Error::new( + ErrorKind::IsADirectory, + "write to a directory path is not supported", + )); + } + let output: oio::OneShotWriter = { Ok(oio::OneShotWriter::new(VercelArtifactsWriter::new( self.core.clone(), diff --git a/core/services/vercel-artifacts/src/docs.md b/core/services/vercel-artifacts/src/docs.md index b531ee1fcf52..3bca41dce703 100644 --- a/core/services/vercel-artifacts/src/docs.md +++ b/core/services/vercel-artifacts/src/docs.md @@ -17,6 +17,7 @@ This service can be used to: Vercel Remote Cache is a Content-Addressable Storage (CAS) designed for caching build artifacts. Because of this, it has the following limitations: - **Folder Operations**: It does not support creating directories (`create_dir`) or listing files (`list`). - **Resource Deletion**: It does not support deleting individual remote cache artifacts (`delete`). Standard cache invalidation is managed automatically by Vercel or triggered locally via cache misses (by changing task hashes). +- **Suffix Range Reads**: `read_with_suffix` is not declared because suffix range reads (`Range: bytes=-N`) have not been verified against the Vercel Remote Cache API. Full reads and standard range reads (`Range: bytes=X-Y`) are supported. ## Configuration From 01706f1039a9533a4cc99fef1d35567bee067b30 Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Wed, 1 Jul 2026 16:41:36 -0700 Subject: [PATCH 4/8] fix(vercel-artifacts): fix write metadata and error handling - write_once now returns Metadata with content_length set to the actual upload size, matching stat behavior and fixing test_write_returns_metadata / test_writer_return_metadata - Remove incorrect Content-Length: 0 header from HEAD (stat) request; HEAD requests carry no body and need no Content-Length - Handle 401 Unauthorized as PermissionDenied (same as 403), so authentication failures surface with a clear error kind --- core/services/vercel-artifacts/src/core.rs | 5 +++-- core/services/vercel-artifacts/src/writer.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/services/vercel-artifacts/src/core.rs b/core/services/vercel-artifacts/src/core.rs index 7da1e73d6c39..ec59f14bbb39 100644 --- a/core/services/vercel-artifacts/src/core.rs +++ b/core/services/vercel-artifacts/src/core.rs @@ -118,7 +118,6 @@ impl VercelArtifactsCore { let auth_header_content = format!("Bearer {}", self.access_token); req = req.header(header::AUTHORIZATION, auth_header_content); - req = req.header(header::CONTENT_LENGTH, 0); req = req .extension(Operation::Stat) @@ -144,7 +143,9 @@ mod error { let (kind, retryable) = match parts.status { StatusCode::NOT_FOUND => (ErrorKind::NotFound, false), - StatusCode::FORBIDDEN => (ErrorKind::PermissionDenied, false), + StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => { + (ErrorKind::PermissionDenied, false) + } StatusCode::INTERNAL_SERVER_ERROR | StatusCode::BAD_GATEWAY | StatusCode::SERVICE_UNAVAILABLE diff --git a/core/services/vercel-artifacts/src/writer.rs b/core/services/vercel-artifacts/src/writer.rs index 201cc06fc7a5..c7023e0aefc2 100644 --- a/core/services/vercel-artifacts/src/writer.rs +++ b/core/services/vercel-artifacts/src/writer.rs @@ -50,15 +50,20 @@ impl VercelArtifactsWriter { impl oio::OneShotWrite for VercelArtifactsWriter { async fn write_once(&self, bs: Buffer) -> Result { + let size = bs.len() as u64; let response = self .core - .vercel_artifacts_put(&self.ctx, self.path.as_str(), bs.len() as u64, bs) + .vercel_artifacts_put(&self.ctx, self.path.as_str(), size, bs) .await?; let status = response.status(); match status { - StatusCode::OK | StatusCode::ACCEPTED => Ok(Metadata::default()), + StatusCode::OK | StatusCode::ACCEPTED => { + let mut meta = Metadata::default(); + meta.set_content_length(size); + Ok(meta) + } _ => Err(parse_error(response)), } } From 5d77d04cca21da37882a2cf88731e99e18b74a77 Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Wed, 1 Jul 2026 17:14:33 -0700 Subject: [PATCH 5/8] fix(vercel-artifacts): use Range GET for stat; drop CAS terminology - Stat: switch HEAD to GET with Range: bytes=0-0 so Content-Range gives the total artifact size. Vercel's HEAD responses omit Content-Length, causing all stat-dependent tests to fail. Accepts 200/206/416 (matches ghac backend pattern). - Remove 'Content-Addressable Storage (CAS)' from docs and comments. Vercel docs don't use that term; artifacts are keyed by task-input hash (not content hash), so the label was misleading. Replace with plain language describing the append-only, hash-keyed design. --- core/services/vercel-artifacts/src/backend.rs | 9 ++++++--- core/services/vercel-artifacts/src/core.rs | 18 +++++++++--------- core/services/vercel-artifacts/src/docs.md | 5 +++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/services/vercel-artifacts/src/backend.rs b/core/services/vercel-artifacts/src/backend.rs index 2a6f1790a7d6..075b61f112ae 100644 --- a/core/services/vercel-artifacts/src/backend.rs +++ b/core/services/vercel-artifacts/src/backend.rs @@ -165,7 +165,7 @@ impl Service for VercelArtifactsBackend { _path: &str, _args: OpCreateDir, ) -> Result { - // Vercel Remote Cache is content-addressable storage (CAS) and does not support folder operations. + // Vercel Remote Cache is append-only and does not support folder operations. Err(Error::new( ErrorKind::Unsupported, "operation is not supported", @@ -182,7 +182,10 @@ impl Service for VercelArtifactsBackend { let status = response.status(); match status { - StatusCode::OK => { + // 206 Partial Content: Range GET succeeded; Content-Range encodes total size. + // 200 OK: server returned full content (Range header ignored); Content-Length set. + // 416 Range Not Satisfiable: artifact exists but is empty (size = 0). + StatusCode::OK | StatusCode::PARTIAL_CONTENT | StatusCode::RANGE_NOT_SATISFIABLE => { let meta = parse_into_metadata(path, response.headers())?; Ok(RpStat::new(meta)) } @@ -225,7 +228,7 @@ impl Service for VercelArtifactsBackend { } fn delete(&self, _ctx: &OperationContext) -> Result { - // Vercel Remote Cache is content-addressable storage (CAS) and does not support resource deletion. + // Vercel Remote Cache is append-only and does not support artifact deletion. Err(Error::new( ErrorKind::Unsupported, "operation is not supported", diff --git a/core/services/vercel-artifacts/src/core.rs b/core/services/vercel-artifacts/src/core.rs index ec59f14bbb39..f57acc26337e 100644 --- a/core/services/vercel-artifacts/src/core.rs +++ b/core/services/vercel-artifacts/src/core.rs @@ -114,16 +114,16 @@ impl VercelArtifactsCore { self.query_string ); - let mut req = Request::head(&url); - - let auth_header_content = format!("Bearer {}", self.access_token); - req = req.header(header::AUTHORIZATION, auth_header_content); - - req = req + // Use a Range request instead of HEAD so that the Content-Range response + // header gives us the total artifact size (Vercel's HEAD responses do not + // include Content-Length). Matches the pattern used by the ghac backend. + let req = Request::get(&url) + .header(header::AUTHORIZATION, format!("Bearer {}", self.access_token)) + .header(header::RANGE, "bytes=0-0") .extension(Operation::Stat) - .extension(ServiceOperation("ArtifactExists")); - - let req = req.body(Buffer::new()).map_err(new_request_build_error)?; + .extension(ServiceOperation("ArtifactExists")) + .body(Buffer::new()) + .map_err(new_request_build_error)?; ctx.http_transport().send(req).await } diff --git a/core/services/vercel-artifacts/src/docs.md b/core/services/vercel-artifacts/src/docs.md index 3bca41dce703..8a438cc9a733 100644 --- a/core/services/vercel-artifacts/src/docs.md +++ b/core/services/vercel-artifacts/src/docs.md @@ -14,9 +14,10 @@ This service can be used to: ## Limitations -Vercel Remote Cache is a Content-Addressable Storage (CAS) designed for caching build artifacts. Because of this, it has the following limitations: +Vercel Remote Cache stores build artifacts addressed by a hash of the task inputs. +Because of its append-only, hash-keyed design, it has the following limitations: - **Folder Operations**: It does not support creating directories (`create_dir`) or listing files (`list`). -- **Resource Deletion**: It does not support deleting individual remote cache artifacts (`delete`). Standard cache invalidation is managed automatically by Vercel or triggered locally via cache misses (by changing task hashes). +- **Resource Deletion**: It does not support deleting individual remote cache artifacts (`delete`). Cache invalidation is handled automatically by Vercel or locally via input changes (which produce a new task hash). - **Suffix Range Reads**: `read_with_suffix` is not declared because suffix range reads (`Range: bytes=-N`) have not been verified against the Vercel Remote Cache API. Full reads and standard range reads (`Range: bytes=X-Y`) are supported. ## Configuration From 7694c268cb173a19bd468568431baae45928396c Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Wed, 1 Jul 2026 17:20:41 -0700 Subject: [PATCH 6/8] test: skip test_writer_write_with_overwrite for vercel-artifacts Vercel Remote Cache returns 200 OK (cache hit, no update) when a PUT is issued for an artifact hash that already exists. The stored content is never updated, so the overwrite assertion always fails. This is the same design constraint that caused ghac to be excluded. Add a parallel guard for vercel-artifacts. --- core/tests/behavior/async_write.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/tests/behavior/async_write.rs b/core/tests/behavior/async_write.rs index d3b5b05e788c..cd946ab6c196 100644 --- a/core/tests/behavior/async_write.rs +++ b/core/tests/behavior/async_write.rs @@ -693,6 +693,12 @@ pub async fn test_writer_write_with_overwrite(op: Operator) -> Result<()> { if op.info().scheme() == services::GHAC_SCHEME { return Ok(()); } + // vercel-artifacts does not support overwrite: a PUT to an existing + // artifact hash returns 200 (cache hit) without updating the stored content. + #[cfg(feature = "services-vercel-artifacts")] + if op.info().scheme() == services::VERCEL_ARTIFACTS_SCHEME { + return Ok(()); + } let path = uuid::Uuid::new_v4().to_string(); let (content_one, _) = gen_bytes(op.info().capability()); From 00999f8c25eea1eb1d49ae5459643313cf049a4d Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Wed, 1 Jul 2026 17:26:23 -0700 Subject: [PATCH 7/8] style: fix rustfmt for vercel_artifacts stat request builder --- core/services/vercel-artifacts/src/core.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/services/vercel-artifacts/src/core.rs b/core/services/vercel-artifacts/src/core.rs index f57acc26337e..ce772e32909b 100644 --- a/core/services/vercel-artifacts/src/core.rs +++ b/core/services/vercel-artifacts/src/core.rs @@ -118,7 +118,10 @@ impl VercelArtifactsCore { // header gives us the total artifact size (Vercel's HEAD responses do not // include Content-Length). Matches the pattern used by the ghac backend. let req = Request::get(&url) - .header(header::AUTHORIZATION, format!("Bearer {}", self.access_token)) + .header( + header::AUTHORIZATION, + format!("Bearer {}", self.access_token), + ) .header(header::RANGE, "bytes=0-0") .extension(Operation::Stat) .extension(ServiceOperation("ArtifactExists")) From 5ce3e6fa1e739fb81aed65883556c2434ff6183c Mon Sep 17 00:00:00 2001 From: Krishna Manchikalapudi Date: Wed, 1 Jul 2026 18:16:16 -0700 Subject: [PATCH 8/8] fix(vercel-artifacts): handle 200 stat without Content-Length via body fallback Vercel may return 200 (ignoring the Range header) with chunked transfer encoding, in which case no Content-Length header is present. parse_into_metadata would then return content_length=0, causing all stat-dependent behavior tests to fail. Split the 200/206/416 match arms so that the 200 branch can fall back to the actual body length when headers carry no size information. --- core/services/vercel-artifacts/src/backend.rs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/core/services/vercel-artifacts/src/backend.rs b/core/services/vercel-artifacts/src/backend.rs index 075b61f112ae..36bcf466ff94 100644 --- a/core/services/vercel-artifacts/src/backend.rs +++ b/core/services/vercel-artifacts/src/backend.rs @@ -182,14 +182,27 @@ impl Service for VercelArtifactsBackend { let status = response.status(); match status { - // 206 Partial Content: Range GET succeeded; Content-Range encodes total size. - // 200 OK: server returned full content (Range header ignored); Content-Length set. - // 416 Range Not Satisfiable: artifact exists but is empty (size = 0). - StatusCode::OK | StatusCode::PARTIAL_CONTENT | StatusCode::RANGE_NOT_SATISFIABLE => { + StatusCode::PARTIAL_CONTENT => { + // 206: Content-Range header encodes total artifact size. + let meta = parse_into_metadata(path, response.headers())?; + Ok(RpStat::new(meta)) + } + StatusCode::OK => { + // 200: server returned full content (Range header not honoured). + // Content-Length may be absent when transfer is chunked; fall back + // to body length so that stat always returns the correct file size. + let (parts, body) = response.into_parts(); + let mut meta = parse_into_metadata(path, &parts.headers)?; + if meta.content_length() == 0 && !body.is_empty() { + meta.set_content_length(body.len() as u64); + } + Ok(RpStat::new(meta)) + } + StatusCode::RANGE_NOT_SATISFIABLE => { + // 416: artifact exists but is empty (size = 0). let meta = parse_into_metadata(path, response.headers())?; Ok(RpStat::new(meta)) } - _ => Err(parse_error(response)), } }