From c888f27b6ab5827408db426ee0a85b57674388e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Ara=C3=BAjo?= Date: Mon, 21 Sep 2026 11:19:08 +0200 Subject: [PATCH 1/3] feat(blobs): add a `--region` flag to the blobs commands The commands opened every store without a region, so they could only ever read the default one, with no way to target a store whose data is held elsewhere. The API path already honours the region, so this passes the flag through to `getStore`. Omitting the flag sends no region at all, exactly as before. Needs `region` on `GetStoreOptions`, which @netlify/blobs 11.1.0 added and main already depends on. --- src/commands/blobs/blobs-delete.ts | 3 +- src/commands/blobs/blobs-get.ts | 4 ++- src/commands/blobs/blobs-list.ts | 4 ++- src/commands/blobs/blobs-set.ts | 4 ++- src/commands/blobs/blobs.ts | 17 ++++++++++ .../integration/commands/blobs/blobs.test.ts | 33 +++++++++++++++++++ 6 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/commands/blobs/blobs-delete.ts b/src/commands/blobs/blobs-delete.ts index 8b877670b7a..0a7fac02e7d 100644 --- a/src/commands/blobs/blobs-delete.ts +++ b/src/commands/blobs/blobs-delete.ts @@ -1,4 +1,4 @@ -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import { chalk, logAndThrowError, log } from '../../utils/command-helpers.js' import { promptBlobDelete } from '../../utils/prompts/blob-delete-prompts.js' @@ -13,6 +13,7 @@ export const blobsDelete = async (storeName: string, key: string, _options: Reco const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: _options.region as GetStoreOptions['region'], siteID: siteInfo.id ?? '', token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs-get.ts b/src/commands/blobs/blobs-get.ts index 0b105e97d05..45d72dd2ded 100644 --- a/src/commands/blobs/blobs-get.ts +++ b/src/commands/blobs/blobs-get.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import { resolve } from 'path' -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import { OptionValues } from 'commander' import { chalk, logAndThrowError } from '../../utils/command-helpers.js' @@ -9,6 +9,7 @@ import BaseCommand from '../base-command.js' interface Options extends OptionValues { output?: string + region?: GetStoreOptions['region'] } export const blobsGet = async (storeName: string, key: string, options: Options, command: BaseCommand) => { @@ -17,6 +18,7 @@ export const blobsGet = async (storeName: string, key: string, options: Options, const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: options.region, siteID: siteInfo?.id ?? '', token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs-list.ts b/src/commands/blobs/blobs-list.ts index 55844976261..f4fe717e15a 100644 --- a/src/commands/blobs/blobs-list.ts +++ b/src/commands/blobs/blobs-list.ts @@ -1,4 +1,4 @@ -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import AsciiTable from 'ascii-table' import { OptionValues } from 'commander' @@ -9,6 +9,7 @@ interface Options extends OptionValues { directories?: boolean json?: boolean prefix?: string + region?: GetStoreOptions['region'] } export const blobsList = async (storeName: string, options: Options, command: BaseCommand) => { @@ -16,6 +17,7 @@ export const blobsList = async (storeName: string, options: Options, command: Ba const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: options.region, siteID: siteInfo.id, token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs-set.ts b/src/commands/blobs/blobs-set.ts index 66b34d21d6c..be8119c4617 100644 --- a/src/commands/blobs/blobs-set.ts +++ b/src/commands/blobs/blobs-set.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import { resolve } from 'path' -import { getStore } from '@netlify/blobs' +import { getStore, type GetStoreOptions } from '@netlify/blobs' import { OptionValues } from 'commander' import { chalk, logAndThrowError, isNodeError, log } from '../../utils/command-helpers.js' @@ -11,6 +11,7 @@ import BaseCommand from '../base-command.js' interface Options extends OptionValues { input?: string force?: string | boolean + region?: GetStoreOptions['region'] } export const blobsSet = async ( @@ -25,6 +26,7 @@ export const blobsSet = async ( const store = getStore({ apiURL: `${api.scheme}://${api.host}`, name: storeName, + region: options.region, siteID: siteInfo.id, token: api.accessToken ?? '', }) diff --git a/src/commands/blobs/blobs.ts b/src/commands/blobs/blobs.ts index 62b77fd203c..39b510cee97 100644 --- a/src/commands/blobs/blobs.ts +++ b/src/commands/blobs/blobs.ts @@ -20,6 +20,10 @@ export const createBlobsCommand = (program: BaseCommand) => { .description(`Deletes an object with a given key, if it exists, from a Netlify Blobs store`) .argument('', 'Name of the store') .argument('', 'Object key') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:delete') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, _options: OptionValues, command: BaseCommand) => { @@ -35,6 +39,10 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Name of the store') .argument('', 'Object key') .option('-O, --output ', 'Defines the filesystem path where the blob data should be persisted') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:get') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, key: string, options: OptionValues, command: BaseCommand) => { @@ -55,6 +63,10 @@ export const createBlobsCommand = (program: BaseCommand) => { `A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned`, ) .option('--json', 'Output list contents as JSON') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:list') .hook('preAction', requiresSiteInfo) .action(async (storeName: string, options: OptionValues, command: BaseCommand) => { @@ -71,6 +83,10 @@ export const createBlobsCommand = (program: BaseCommand) => { .argument('', 'Object key') .argument('[value...]', 'Object value') .option('-i, --input ', 'Defines the filesystem path where the blob data should be read from') + .option( + '--region ', + "The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used", + ) .alias('blob:set') .hook('preAction', requiresSiteInfo) @@ -98,6 +114,7 @@ For more information about Netlify Blobs, see ${terminalLink(docsUrl, docsUrl, { 'netlify blobs:delete my-store my-key', 'netlify blobs:list my-store', 'netlify blobs:list my-store --json', + 'netlify blobs:list my-store --region eu-central-1', ]) .action(blobs) } diff --git a/tests/integration/commands/blobs/blobs.test.ts b/tests/integration/commands/blobs/blobs.test.ts index 6fa7187a07f..c80ac7c26aa 100644 --- a/tests/integration/commands/blobs/blobs.test.ts +++ b/tests/integration/commands/blobs/blobs.test.ts @@ -27,6 +27,7 @@ describe('blobs:* commands', () => { const directory = temporaryDirectory() let server: BlobsServer + const requestedURLs: string[] = [] const routes: Route[] = [ { path: 'sites/site_id', response: siteInfo }, @@ -50,6 +51,7 @@ describe('blobs:* commands', () => { method: 'all', path: 'blobs/{*splat}', response: (req, res) => { + requestedURLs.push(req.url) blobsProxy.web(req, res, { target: `http://localhost:${address.port}` }) }, }) @@ -118,5 +120,36 @@ describe('blobs:* commands', () => { }), ).rejects.toThrowError('Error: Blob my-key does not exist in store my-store') }) + + test('should send the region to the API when one is given', async ({ fixture }) => { + const region = 'eu-central-1' + + requestedURLs.length = 0 + await fixture.callCli(['blobs:list', 'my-store', '--region', region, '--json'], { + offline: false, + parseJson: true, + }) + + expect(requestedURLs.some((url) => url.includes(`region=${region}`))).toBe(true) + }) + + test('should not send a region when none is given', async ({ fixture }) => { + requestedURLs.length = 0 + await fixture.callCli(['blobs:list', 'my-store', '--json'], { + offline: false, + parseJson: true, + }) + + expect(requestedURLs.length).toBeGreaterThan(0) + expect(requestedURLs.some((url) => url.includes('region'))).toBe(false) + }) + + test('should reject a region the Blobs client does not support', async ({ fixture }) => { + await expect( + fixture.callCli(['blobs:list', 'my-store', '--region', 'mars-north-1', '--json'], { + offline: false, + }), + ).rejects.toThrowError('not a supported Netlify Blobs region') + }) }) }) From 653ec0763d551146c377d51a96523a5cdb660457 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Ara=C3=BAjo?= Date: Mon, 21 Sep 2026 17:54:34 +0200 Subject: [PATCH 2/3] docs: sync blobs command docs for the region flag Generated by `npm run docs`, which the verify-docs check regenerates and compares. --- docs/commands/blobs.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index a0a6fb52b4f..4ed56ef209e 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -39,6 +39,7 @@ netlify blobs:set my-store my-key --input ./some-file.txt netlify blobs:delete my-store my-key netlify blobs:list my-store netlify blobs:list my-store --json +netlify blobs:list my-store --region eu-central-1 ``` --- @@ -61,6 +62,7 @@ netlify blobs:delete - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in @@ -84,6 +86,7 @@ netlify blobs:get - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `output` (*string*) - Defines the filesystem path where the blob data should be persisted +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in @@ -108,6 +111,7 @@ netlify blobs:list - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output list contents as JSON - `prefix` (*string*) - A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in @@ -133,6 +137,7 @@ netlify blobs:set - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. - `input` (*string*) - Defines the filesystem path where the blob data should be read from +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in From 030833953095f112f17a36c3870adaddd1098015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Ara=C3=BAjo?= Date: Mon, 21 Sep 2026 19:03:17 +0200 Subject: [PATCH 3/3] docs: correct generated flag order for blobs:list Regenerated on the Node version CI pins. `sortOptions` returns -1 whenever either side is a base flag, so the order it produces depends on the sort implementation and differs between Node versions. --- docs/commands/blobs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index 4ed56ef209e..de0ddc7425b 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -111,9 +111,9 @@ netlify blobs:list - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output list contents as JSON - `prefix` (*string*) - A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned -- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `region` (*string*) - The region where the store data is held, such as 'eu-central-1'; when omitted, the default region is used --- ## `blobs:set`