From 765c59f360e5b5cd717edf9fbab1c07044a98386 Mon Sep 17 00:00:00 2001 From: carjessu-trm Date: Tue, 24 Mar 2026 12:20:06 +0100 Subject: [PATCH] fix: use Azurite-compatible URL format for signed_upload_url `actions/cache@v4.2.0+` hardcodes `useAzureSdk: true` in `saveCacheV2()`, which passes the `signed_upload_url` from `CreateCacheEntry` to the Azure `BlobClient` constructor. The constructor expects URLs in Azure Blob format (or Azurite format for non-Azure hosts) and fails to parse the current `/upload/{id}` path with: "Unable to extract blobName and containerName with provided information." This changes the upload URL from: `{API_BASE_URL}/upload/{id}` to: `{API_BASE_URL}/devstoreaccount1/upload/{id}` The Azurite-style path (`/devstoreaccount1//`) is recognized by the Azure SDK as a valid storage emulator URL, allowing it to extract: - accountName: "devstoreaccount1" - containerName: "upload" - blobName: "{id}" The upload handler at the new path is identical to the existing one. The original `/upload/{id}` route is preserved for backward compatibility. Fixes #203 Made-with: Cursor --- .../devstoreaccount1/upload/[uploadId].put.ts | 70 +++++++++++++++++++ .../CreateCacheEntry.post.ts | 2 +- 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 routes/devstoreaccount1/upload/[uploadId].put.ts diff --git a/routes/devstoreaccount1/upload/[uploadId].put.ts b/routes/devstoreaccount1/upload/[uploadId].put.ts new file mode 100644 index 0000000..8857191 --- /dev/null +++ b/routes/devstoreaccount1/upload/[uploadId].put.ts @@ -0,0 +1,70 @@ +import type { ReadableStream } from 'node:stream/web' +import { Buffer } from 'node:buffer' +import { randomUUID } from 'node:crypto' + +import { z } from 'zod' +import { logger } from '~/lib/logger' + +import { getStorage } from '~/lib/storage' + +const pathParamsSchema = z.object({ + uploadId: z.coerce.number(), +}) + +export default defineEventHandler(async (event) => { + const parsedPathParams = pathParamsSchema.safeParse(event.context.params) + if (!parsedPathParams.success) + throw createError({ + statusCode: 400, + statusMessage: `Invalid path parameters: ${parsedPathParams.error.message}`, + }) + + if (getQuery(event).comp === 'blocklist') { + setResponseStatus(event, 201) + // prevent random EOF error with in tonistiigi/go-actions-cache caused by missing request id + setHeader(event, 'x-ms-request-id', randomUUID()) + return + } + + const blockId = getQuery(event)?.blockid as string + // if no block id, upload smaller than chunk size + const chunkIndex = blockId ? getChunkIndexFromBlockId(blockId) : 0 + if (chunkIndex === undefined) + throw createError({ + statusCode: 400, + statusMessage: `Invalid block id: ${blockId}`, + }) + + const { uploadId } = parsedPathParams.data + + const stream = getRequestWebStream(event) + if (!stream) { + logger.debug('Upload: Request body is not a stream') + throw createError({ statusCode: 400, statusMessage: 'Request body must be a stream' }) + } + + const storage = await getStorage() + await storage.uploadPart(uploadId, chunkIndex, stream as ReadableStream) + + // prevent random EOF error with in tonistiigi/go-actions-cache caused by missing request id + setHeader(event, 'x-ms-request-id', randomUUID()) + setResponseStatus(event, 201) +}) + +function getChunkIndexFromBlockId(blockIdBase64: string) { + const base64Decoded = Buffer.from(blockIdBase64, 'base64') + + // 64 bytes used by docker buildx + // 48 bytes used by everything else + if (base64Decoded.length === 64) { + return base64Decoded.readUInt32BE(16) + } else if (base64Decoded.length === 48) { + const decoded = base64Decoded.toString('utf8') + + // slice off uuid and convert to number + const index = Number.parseInt(decoded.slice(36)) + if (Number.isNaN(index)) return + + return index + } +} diff --git a/routes/twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry.post.ts b/routes/twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry.post.ts index 8821aff..be981fc 100644 --- a/routes/twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry.post.ts +++ b/routes/twirp/github.actions.results.api.v1.CacheService/CreateCacheEntry.post.ts @@ -34,6 +34,6 @@ export default defineEventHandler(async (event) => { return { ok: true, - signed_upload_url: `${env.API_BASE_URL}/upload/${upload.id}`, + signed_upload_url: `${env.API_BASE_URL}/devstoreaccount1/upload/${upload.id}`, } })